Compare commits
2 Commits
d92509b5f2
...
15312f498b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15312f498b | ||
|
|
377443eec3 |
@ -19,6 +19,7 @@
|
|||||||
- 腾讯云 IM 负责客户端长连接、房间群消息、公屏、单聊、离线/漫游和全球接入;
|
- 腾讯云 IM 负责客户端长连接、房间群消息、公屏、单聊、离线/漫游和全球接入;
|
||||||
- `wallet-service` 负责账务语义。`SendGift` 必须先扣费成功,再落房间表现。
|
- `wallet-service` 负责账务语义。`SendGift` 必须先扣费成功,再落房间表现。
|
||||||
- `activity-service` 通过 room outbox 事件消费,不侵入 Room Cell 主链路。
|
- `activity-service` 通过 room outbox 事件消费,不侵入 Room Cell 主链路。
|
||||||
|
- `activity-service/internal/service/message` 是 App 系统/活动消息 inbox 的唯一业务 owner;活动发通知必须走 `CreateSystemNotice`、`CreateActivityNotice` 或 `CreateNoticeFanout`,后台和其他服务不能直接写 `user_inbox_messages` / `message_fanout_jobs`。
|
||||||
- `notice-service` 负责从各 owner service 的 outbox/事件源读取事实并投递私有通知、腾讯云 IM 单聊、后续 push/inbox;钱包、活动和房间不能直接耦合私有通知投递实现。
|
- `notice-service` 负责从各 owner service 的 outbox/事件源读取事实并投递私有通知、腾讯云 IM 单聊、后续 push/inbox;钱包、活动和房间不能直接耦合私有通知投递实现。
|
||||||
- `user-service` 负责用户主数据,不应该成为房间高频流程的同步中心。
|
- `user-service` 负责用户主数据,不应该成为房间高频流程的同步中心。
|
||||||
- `cron-service` 只负责后台调度、任务运行记录和通过内部 gRPC 触发 owner service 批处理;不能直接拥有房间、账务、用户或活动业务状态。
|
- `cron-service` 只负责后台调度、任务运行记录和通过内部 gRPC 触发 owner service 批处理;不能直接拥有房间、账务、用户或活动业务状态。
|
||||||
@ -70,6 +71,7 @@
|
|||||||
- 修改 `api/proto` 后必须运行 `make proto`,并提交生成后的 `.pb.go` / `_grpc.pb.go`。
|
- 修改 `api/proto` 后必须运行 `make proto`,并提交生成后的 `.pb.go` / `_grpc.pb.go`。
|
||||||
- 不要在服务之间直接 import 对方 `internal` 包。跨服务只走 protobuf client 或明确的接口抽象。
|
- 不要在服务之间直接 import 对方 `internal` 包。跨服务只走 protobuf client 或明确的接口抽象。
|
||||||
- `server/admin` 不能 import `hyapp/services/*` 或 `hyapp/pkg/*`;后台专属基础设施放在 `server/admin/internal/platform`。
|
- `server/admin` 不能 import `hyapp/services/*` 或 `hyapp/pkg/*`;后台专属基础设施放在 `server/admin/internal/platform`。
|
||||||
|
- 系统/活动消息协议只使用后端 inbox 的 `message_type=system|activity`、`title`、`summary`、`body`、`action_type`、`action_param`、`metadata_json` 等字段;后台全服通知必须创建 fanout job,不能在 admin HTTP handler 中同步遍历用户发消息。
|
||||||
|
|
||||||
## Port Rules
|
## Port Rules
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,13 @@ HY Voice Room 是语音房后端 monorepo。当前架构不再自研 IM 长连
|
|||||||
- 下游消费 owner outbox 必须使用本服务独立 RocketMQ consumer group,并在本服务库内用幂等表或唯一键记录已消费 `event_id`;消费位点不能写回或复用 owner service 的 outbox 状态。
|
- 下游消费 owner outbox 必须使用本服务独立 RocketMQ consumer group,并在本服务库内用幂等表或唯一键记录已消费 `event_id`;消费位点不能写回或复用 owner service 的 outbox 状态。
|
||||||
- 后台统计查询面:`server/admin` 调 `statistics-service` 内部 HTTP API;`gateway-service` 是 App 入口,除非有明确 App 产品需求,否则不暴露后台数据大屏统计。
|
- 后台统计查询面:`server/admin` 调 `statistics-service` 内部 HTTP API;`gateway-service` 是 App 入口,除非有明确 App 产品需求,否则不暴露后台数据大屏统计。
|
||||||
|
|
||||||
|
App 系统/活动消息:
|
||||||
|
|
||||||
|
- Flutter 消息页固定展示腾讯云 IM 用户消息、后端系统消息和后端活动消息三个入口;系统/活动页通过 `gateway-service` 读取 `activity-service` 的 inbox,只按当前客户端能力展示标题、摘要/正文和时间。
|
||||||
|
- `activity-service/internal/service/message` 是系统/活动消息 owner。单人消息使用 `CreateSystemNotice` / `CreateActivityNotice`,全服或范围通知使用 `CreateNoticeFanout` 创建 `message_fanout_jobs`,由 cron 触发 fanout worker 分批写入 `user_inbox_messages`。
|
||||||
|
- 活动业务发通知时传 `producer_event_id` 或 `command_id` 作为幂等键,`message_type` 只允许 `system` 或 `activity`;跳转参数使用已有 `action_type` 白名单,例如 `activity_detail`、`room_detail`、`user_profile`、`app_h5`。
|
||||||
|
- `server/admin` 的运营管理/全服通知只通过 `MessageInboxService.CreateFanoutJob` 创建后台 fanout 命令,不直接写 `hyapp_activity` 表,也不在 HTTP 请求里同步遍历用户。
|
||||||
|
|
||||||
## Time Model
|
## Time Model
|
||||||
|
|
||||||
项目全局时间模型固定为 UTC + epoch milliseconds。除客户端展示外,App 后端所有按天、按周、按月归档或结算的业务口径都按 UTC,不按中国时区、服务器本地时区、用户设备时区或 IP 推断时区切分。
|
项目全局时间模型固定为 UTC + epoch milliseconds。除客户端展示外,App 后端所有按天、按周、按月归档或结算的业务口径都按 UTC,不按中国时区、服务器本地时区、用户设备时区或 IP 推断时区切分。
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -2042,6 +2042,254 @@ message ListWeeklyStarHistoryResponse {
|
|||||||
int64 server_time_ms = 2;
|
int64 server_time_ms = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CPWeeklyRankReward 是 CP 周榜名次对应的资源组奖励。
|
||||||
|
message CPWeeklyRankReward {
|
||||||
|
int32 rank_no = 1;
|
||||||
|
int64 resource_group_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPWeeklyRankConfig 是 CP 周榜的全局启停和 Top 奖励配置。
|
||||||
|
message CPWeeklyRankConfig {
|
||||||
|
string activity_code = 1;
|
||||||
|
bool enabled = 2;
|
||||||
|
string relation_type = 3;
|
||||||
|
int32 top_count = 4;
|
||||||
|
repeated CPWeeklyRankReward rewards = 5;
|
||||||
|
int64 updated_by_admin_id = 6;
|
||||||
|
int64 created_at_ms = 7;
|
||||||
|
int64 updated_at_ms = 8;
|
||||||
|
string app_code = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPWeeklyRankUser 是 H5 周榜展示的一方用户快照。
|
||||||
|
message CPWeeklyRankUser {
|
||||||
|
int64 user_id = 1;
|
||||||
|
string display_user_id = 2;
|
||||||
|
string username = 3;
|
||||||
|
string avatar = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPWeeklyRankEntry 是 activity-service 从 user-service 读取并返回 H5 的周榜条目。
|
||||||
|
message CPWeeklyRankEntry {
|
||||||
|
int64 rank = 1;
|
||||||
|
string relationship_id = 2;
|
||||||
|
string relation_type = 3;
|
||||||
|
int64 score = 4;
|
||||||
|
CPWeeklyRankUser user_a = 5;
|
||||||
|
CPWeeklyRankUser user_b = 6;
|
||||||
|
int64 formed_at_ms = 7;
|
||||||
|
int64 updated_at_ms = 8;
|
||||||
|
int64 first_scored_at_ms = 9;
|
||||||
|
int64 last_scored_at_ms = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPWeeklyRankSettlement 是每周 cron 固化的一条用户发奖事实。
|
||||||
|
message CPWeeklyRankSettlement {
|
||||||
|
string settlement_id = 1;
|
||||||
|
int64 period_start_ms = 2;
|
||||||
|
int64 period_end_ms = 3;
|
||||||
|
int32 rank_no = 4;
|
||||||
|
string relationship_id = 5;
|
||||||
|
int64 user_id = 6;
|
||||||
|
int64 score = 7;
|
||||||
|
int64 resource_group_id = 8;
|
||||||
|
string wallet_command_id = 9;
|
||||||
|
string wallet_grant_id = 10;
|
||||||
|
string status = 11;
|
||||||
|
string failure_reason = 12;
|
||||||
|
int32 attempt_count = 13;
|
||||||
|
int64 created_at_ms = 14;
|
||||||
|
int64 updated_at_ms = 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetCPWeeklyRankStatusRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
int64 now_ms = 2;
|
||||||
|
int32 limit = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetCPWeeklyRankStatusResponse {
|
||||||
|
CPWeeklyRankConfig config = 1;
|
||||||
|
repeated CPWeeklyRankEntry entries = 2;
|
||||||
|
int64 period_start_ms = 3;
|
||||||
|
int64 period_end_ms = 4;
|
||||||
|
int64 server_time_ms = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetCPWeeklyRankConfigRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetCPWeeklyRankConfigResponse {
|
||||||
|
CPWeeklyRankConfig config = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateCPWeeklyRankConfigRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
CPWeeklyRankConfig config = 2;
|
||||||
|
int64 operator_admin_id = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateCPWeeklyRankConfigResponse {
|
||||||
|
CPWeeklyRankConfig config = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListCPWeeklyRankSettlementsRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
int64 period_start_ms = 2;
|
||||||
|
int64 period_end_ms = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListCPWeeklyRankSettlementsResponse {
|
||||||
|
repeated CPWeeklyRankSettlement settlements = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgencyOpeningReward 是代理开业活动房间流水档位对应的金币奖励。
|
||||||
|
message AgencyOpeningReward {
|
||||||
|
// rank_no 保留为档位展示顺序和旧字段兼容,结算不再按榜单名次发奖。
|
||||||
|
int32 rank_no = 1;
|
||||||
|
int64 reward_coin_amount = 2;
|
||||||
|
int64 threshold_coin_spent = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgencyOpeningCycle 是后台配置的代理开业活动周期。
|
||||||
|
message AgencyOpeningCycle {
|
||||||
|
string cycle_id = 1;
|
||||||
|
string activity_code = 2;
|
||||||
|
string title = 3;
|
||||||
|
string status = 4;
|
||||||
|
int64 start_ms = 5;
|
||||||
|
int64 end_ms = 6;
|
||||||
|
int32 min_host_count = 7;
|
||||||
|
int32 max_agency_age_days = 8;
|
||||||
|
repeated AgencyOpeningReward rewards = 9;
|
||||||
|
int64 created_by_admin_id = 10;
|
||||||
|
int64 updated_by_admin_id = 11;
|
||||||
|
int64 settled_at_ms = 12;
|
||||||
|
int64 created_at_ms = 13;
|
||||||
|
int64 updated_at_ms = 14;
|
||||||
|
string app_code = 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgencyOpeningApplication 是代理提交开业活动申请后的流水和发奖事实。
|
||||||
|
message AgencyOpeningApplication {
|
||||||
|
string application_id = 1;
|
||||||
|
string cycle_id = 2;
|
||||||
|
int64 agency_id = 3;
|
||||||
|
int64 agency_owner_user_id = 4;
|
||||||
|
string agency_name = 5;
|
||||||
|
int32 host_count = 6;
|
||||||
|
int64 agency_created_at_ms = 7;
|
||||||
|
string status = 8;
|
||||||
|
int64 score_coin_amount = 9;
|
||||||
|
int32 reward_rank_no = 10;
|
||||||
|
int64 reward_coin_amount = 11;
|
||||||
|
string wallet_command_id = 12;
|
||||||
|
string wallet_transaction_id = 13;
|
||||||
|
string failure_reason = 14;
|
||||||
|
int64 applied_at_ms = 15;
|
||||||
|
int64 granted_at_ms = 16;
|
||||||
|
int64 updated_at_ms = 17;
|
||||||
|
int64 reward_threshold_coin_spent = 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgencyOpeningAgencySnapshot 是申请页展示和资格判断使用的代理快照。
|
||||||
|
message AgencyOpeningAgencySnapshot {
|
||||||
|
int64 agency_id = 1;
|
||||||
|
int64 owner_user_id = 2;
|
||||||
|
string name = 3;
|
||||||
|
string status = 4;
|
||||||
|
int32 host_count = 5;
|
||||||
|
int64 agency_created_at_ms = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListAgencyOpeningCyclesRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
string status = 2;
|
||||||
|
int64 start_ms = 3;
|
||||||
|
int64 end_ms = 4;
|
||||||
|
int32 page = 5;
|
||||||
|
int32 page_size = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListAgencyOpeningCyclesResponse {
|
||||||
|
repeated AgencyOpeningCycle cycles = 1;
|
||||||
|
int64 total = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetAgencyOpeningCycleRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
string cycle_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetAgencyOpeningCycleResponse {
|
||||||
|
AgencyOpeningCycle cycle = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpsertAgencyOpeningCycleRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
AgencyOpeningCycle cycle = 2;
|
||||||
|
int64 operator_admin_id = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpsertAgencyOpeningCycleResponse {
|
||||||
|
AgencyOpeningCycle cycle = 1;
|
||||||
|
bool created = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetAgencyOpeningCycleStatusRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
string cycle_id = 2;
|
||||||
|
string status = 3;
|
||||||
|
int64 operator_admin_id = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetAgencyOpeningCycleStatusResponse {
|
||||||
|
AgencyOpeningCycle cycle = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListAgencyOpeningApplicationsRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
string cycle_id = 2;
|
||||||
|
string status = 3;
|
||||||
|
int64 agency_id = 4;
|
||||||
|
int64 agency_owner_user_id = 5;
|
||||||
|
int32 page = 6;
|
||||||
|
int32 page_size = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListAgencyOpeningApplicationsResponse {
|
||||||
|
repeated AgencyOpeningApplication applications = 1;
|
||||||
|
int64 total = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetAgencyOpeningStatusRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
int64 user_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetAgencyOpeningStatusResponse {
|
||||||
|
AgencyOpeningCycle cycle = 1;
|
||||||
|
AgencyOpeningApplication application = 2;
|
||||||
|
bool enabled = 3;
|
||||||
|
bool eligible = 4;
|
||||||
|
bool joined = 5;
|
||||||
|
string ineligible_reason = 6;
|
||||||
|
int64 server_time_ms = 7;
|
||||||
|
AgencyOpeningAgencySnapshot agency = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ApplyAgencyOpeningRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
int64 user_id = 2;
|
||||||
|
string command_id = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ApplyAgencyOpeningResponse {
|
||||||
|
AgencyOpeningApplication application = 1;
|
||||||
|
bool created = 2;
|
||||||
|
}
|
||||||
|
|
||||||
// ActivityService 只暴露同步查询,不把事件消费伪装成 RPC。
|
// ActivityService 只暴露同步查询,不把事件消费伪装成 RPC。
|
||||||
service ActivityService {
|
service ActivityService {
|
||||||
rpc PingActivity(PingActivityRequest) returns (PingActivityResponse);
|
rpc PingActivity(PingActivityRequest) returns (PingActivityResponse);
|
||||||
@ -2066,6 +2314,8 @@ service ActivityCronService {
|
|||||||
rpc ProcessAchievementRewardBatch(CronBatchRequest) returns (CronBatchResponse);
|
rpc ProcessAchievementRewardBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||||
rpc ProcessRoomTurnoverRewardSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
rpc ProcessRoomTurnoverRewardSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||||
rpc ProcessWeeklyStarSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
rpc ProcessWeeklyStarSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||||
|
rpc ProcessCPWeeklyRankSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||||
|
rpc ProcessAgencyOpeningSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskService 拥有 App 任务查询、事件进度消费和领奖状态。
|
// TaskService 拥有 App 任务查询、事件进度消费和领奖状态。
|
||||||
@ -2112,6 +2362,18 @@ service WeeklyStarService {
|
|||||||
rpc ListWeeklyStarHistory(ListWeeklyStarHistoryRequest) returns (ListWeeklyStarHistoryResponse);
|
rpc ListWeeklyStarHistory(ListWeeklyStarHistoryRequest) returns (ListWeeklyStarHistoryResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CPWeeklyRankService owns H5 reads for CP weekly rank and reward config.
|
||||||
|
service CPWeeklyRankService {
|
||||||
|
rpc GetCPWeeklyRankStatus(GetCPWeeklyRankStatusRequest) returns (GetCPWeeklyRankStatusResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgencyOpeningService owns App/H5 reads and application writes for agency opening activity.
|
||||||
|
service AgencyOpeningService {
|
||||||
|
rpc GetAgencyOpeningStatus(GetAgencyOpeningStatusRequest) returns (GetAgencyOpeningStatusResponse);
|
||||||
|
rpc ApplyAgencyOpening(ApplyAgencyOpeningRequest) returns (ApplyAgencyOpeningResponse);
|
||||||
|
rpc ListAgencyOpeningLeaderboard(ListAgencyOpeningApplicationsRequest) returns (ListAgencyOpeningApplicationsResponse);
|
||||||
|
}
|
||||||
|
|
||||||
// BroadcastService owns server-side Tencent IM global/region broadcast delivery.
|
// BroadcastService owns server-side Tencent IM global/region broadcast delivery.
|
||||||
service BroadcastService {
|
service BroadcastService {
|
||||||
rpc EnsureBroadcastGroups(EnsureBroadcastGroupsRequest) returns (EnsureBroadcastGroupsResponse);
|
rpc EnsureBroadcastGroups(EnsureBroadcastGroupsRequest) returns (EnsureBroadcastGroupsResponse);
|
||||||
@ -2206,6 +2468,23 @@ service AdminWeeklyStarService {
|
|||||||
rpc ListWeeklyStarSettlements(ListWeeklyStarSettlementsRequest) returns (ListWeeklyStarSettlementsResponse);
|
rpc ListWeeklyStarSettlements(ListWeeklyStarSettlementsRequest) returns (ListWeeklyStarSettlementsResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminCPWeeklyRankService 是后台配置 CP 周榜启停、奖励和查询发奖事实的入口。
|
||||||
|
service AdminCPWeeklyRankService {
|
||||||
|
rpc GetCPWeeklyRankConfig(GetCPWeeklyRankConfigRequest) returns (GetCPWeeklyRankConfigResponse);
|
||||||
|
rpc UpdateCPWeeklyRankConfig(UpdateCPWeeklyRankConfigRequest) returns (UpdateCPWeeklyRankConfigResponse);
|
||||||
|
rpc ListCPWeeklyRankSettlements(ListCPWeeklyRankSettlementsRequest) returns (ListCPWeeklyRankSettlementsResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminAgencyOpeningService 是后台配置开业周期、查看申请和榜单发奖结果的入口。
|
||||||
|
service AdminAgencyOpeningService {
|
||||||
|
rpc ListAgencyOpeningCycles(ListAgencyOpeningCyclesRequest) returns (ListAgencyOpeningCyclesResponse);
|
||||||
|
rpc CreateAgencyOpeningCycle(UpsertAgencyOpeningCycleRequest) returns (UpsertAgencyOpeningCycleResponse);
|
||||||
|
rpc GetAgencyOpeningCycle(GetAgencyOpeningCycleRequest) returns (GetAgencyOpeningCycleResponse);
|
||||||
|
rpc UpdateAgencyOpeningCycle(UpsertAgencyOpeningCycleRequest) returns (UpsertAgencyOpeningCycleResponse);
|
||||||
|
rpc SetAgencyOpeningCycleStatus(SetAgencyOpeningCycleStatusRequest) returns (SetAgencyOpeningCycleStatusResponse);
|
||||||
|
rpc ListAgencyOpeningApplications(ListAgencyOpeningApplicationsRequest) returns (ListAgencyOpeningApplicationsResponse);
|
||||||
|
}
|
||||||
|
|
||||||
// SevenDayCheckInService 拥有 App 七日签到查询和签到命令。
|
// SevenDayCheckInService 拥有 App 七日签到查询和签到命令。
|
||||||
service SevenDayCheckInService {
|
service SevenDayCheckInService {
|
||||||
rpc GetSevenDayCheckInStatus(GetSevenDayCheckInStatusRequest) returns (GetSevenDayCheckInStatusResponse);
|
rpc GetSevenDayCheckInStatus(GetSevenDayCheckInStatusRequest) returns (GetSevenDayCheckInStatusResponse);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.2
|
// - protoc-gen-go-grpc v1.6.2
|
||||||
// - protoc v5.29.2
|
// - protoc v7.35.0
|
||||||
// source: proto/activity/v1/activity.proto
|
// source: proto/activity/v1/activity.proto
|
||||||
|
|
||||||
package activityv1
|
package activityv1
|
||||||
@ -502,6 +502,8 @@ const (
|
|||||||
ActivityCronService_ProcessAchievementRewardBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessAchievementRewardBatch"
|
ActivityCronService_ProcessAchievementRewardBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessAchievementRewardBatch"
|
||||||
ActivityCronService_ProcessRoomTurnoverRewardSettlementBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessRoomTurnoverRewardSettlementBatch"
|
ActivityCronService_ProcessRoomTurnoverRewardSettlementBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessRoomTurnoverRewardSettlementBatch"
|
||||||
ActivityCronService_ProcessWeeklyStarSettlementBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessWeeklyStarSettlementBatch"
|
ActivityCronService_ProcessWeeklyStarSettlementBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessWeeklyStarSettlementBatch"
|
||||||
|
ActivityCronService_ProcessCPWeeklyRankSettlementBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessCPWeeklyRankSettlementBatch"
|
||||||
|
ActivityCronService_ProcessAgencyOpeningSettlementBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessAgencyOpeningSettlementBatch"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ActivityCronServiceClient is the client API for ActivityCronService service.
|
// ActivityCronServiceClient is the client API for ActivityCronService service.
|
||||||
@ -515,6 +517,8 @@ type ActivityCronServiceClient interface {
|
|||||||
ProcessAchievementRewardBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error)
|
ProcessAchievementRewardBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error)
|
||||||
ProcessRoomTurnoverRewardSettlementBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error)
|
ProcessRoomTurnoverRewardSettlementBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error)
|
||||||
ProcessWeeklyStarSettlementBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error)
|
ProcessWeeklyStarSettlementBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error)
|
||||||
|
ProcessCPWeeklyRankSettlementBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error)
|
||||||
|
ProcessAgencyOpeningSettlementBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type activityCronServiceClient struct {
|
type activityCronServiceClient struct {
|
||||||
@ -575,6 +579,26 @@ func (c *activityCronServiceClient) ProcessWeeklyStarSettlementBatch(ctx context
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *activityCronServiceClient) ProcessCPWeeklyRankSettlementBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CronBatchResponse)
|
||||||
|
err := c.cc.Invoke(ctx, ActivityCronService_ProcessCPWeeklyRankSettlementBatch_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *activityCronServiceClient) ProcessAgencyOpeningSettlementBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CronBatchResponse)
|
||||||
|
err := c.cc.Invoke(ctx, ActivityCronService_ProcessAgencyOpeningSettlementBatch_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ActivityCronServiceServer is the server API for ActivityCronService service.
|
// ActivityCronServiceServer is the server API for ActivityCronService service.
|
||||||
// All implementations must embed UnimplementedActivityCronServiceServer
|
// All implementations must embed UnimplementedActivityCronServiceServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@ -586,6 +610,8 @@ type ActivityCronServiceServer interface {
|
|||||||
ProcessAchievementRewardBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
ProcessAchievementRewardBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
||||||
ProcessRoomTurnoverRewardSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
ProcessRoomTurnoverRewardSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
||||||
ProcessWeeklyStarSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
ProcessWeeklyStarSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
||||||
|
ProcessCPWeeklyRankSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
||||||
|
ProcessAgencyOpeningSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
||||||
mustEmbedUnimplementedActivityCronServiceServer()
|
mustEmbedUnimplementedActivityCronServiceServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -611,6 +637,12 @@ func (UnimplementedActivityCronServiceServer) ProcessRoomTurnoverRewardSettlemen
|
|||||||
func (UnimplementedActivityCronServiceServer) ProcessWeeklyStarSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
func (UnimplementedActivityCronServiceServer) ProcessWeeklyStarSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method ProcessWeeklyStarSettlementBatch not implemented")
|
return nil, status.Error(codes.Unimplemented, "method ProcessWeeklyStarSettlementBatch not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedActivityCronServiceServer) ProcessCPWeeklyRankSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ProcessCPWeeklyRankSettlementBatch not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedActivityCronServiceServer) ProcessAgencyOpeningSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ProcessAgencyOpeningSettlementBatch not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedActivityCronServiceServer) mustEmbedUnimplementedActivityCronServiceServer() {}
|
func (UnimplementedActivityCronServiceServer) mustEmbedUnimplementedActivityCronServiceServer() {}
|
||||||
func (UnimplementedActivityCronServiceServer) testEmbeddedByValue() {}
|
func (UnimplementedActivityCronServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@ -722,6 +754,42 @@ func _ActivityCronService_ProcessWeeklyStarSettlementBatch_Handler(srv interface
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _ActivityCronService_ProcessCPWeeklyRankSettlementBatch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CronBatchRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ActivityCronServiceServer).ProcessCPWeeklyRankSettlementBatch(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: ActivityCronService_ProcessCPWeeklyRankSettlementBatch_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ActivityCronServiceServer).ProcessCPWeeklyRankSettlementBatch(ctx, req.(*CronBatchRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ActivityCronService_ProcessAgencyOpeningSettlementBatch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CronBatchRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ActivityCronServiceServer).ProcessAgencyOpeningSettlementBatch(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: ActivityCronService_ProcessAgencyOpeningSettlementBatch_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ActivityCronServiceServer).ProcessAgencyOpeningSettlementBatch(ctx, req.(*CronBatchRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// ActivityCronService_ServiceDesc is the grpc.ServiceDesc for ActivityCronService service.
|
// ActivityCronService_ServiceDesc is the grpc.ServiceDesc for ActivityCronService service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@ -749,6 +817,14 @@ var ActivityCronService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ProcessWeeklyStarSettlementBatch",
|
MethodName: "ProcessWeeklyStarSettlementBatch",
|
||||||
Handler: _ActivityCronService_ProcessWeeklyStarSettlementBatch_Handler,
|
Handler: _ActivityCronService_ProcessWeeklyStarSettlementBatch_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ProcessCPWeeklyRankSettlementBatch",
|
||||||
|
Handler: _ActivityCronService_ProcessCPWeeklyRankSettlementBatch_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ProcessAgencyOpeningSettlementBatch",
|
||||||
|
Handler: _ActivityCronService_ProcessAgencyOpeningSettlementBatch_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "proto/activity/v1/activity.proto",
|
Metadata: "proto/activity/v1/activity.proto",
|
||||||
@ -1923,6 +1999,294 @@ var WeeklyStarService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
Metadata: "proto/activity/v1/activity.proto",
|
Metadata: "proto/activity/v1/activity.proto",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
CPWeeklyRankService_GetCPWeeklyRankStatus_FullMethodName = "/hyapp.activity.v1.CPWeeklyRankService/GetCPWeeklyRankStatus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CPWeeklyRankServiceClient is the client API for CPWeeklyRankService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
//
|
||||||
|
// CPWeeklyRankService owns H5 reads for CP weekly rank and reward config.
|
||||||
|
type CPWeeklyRankServiceClient interface {
|
||||||
|
GetCPWeeklyRankStatus(ctx context.Context, in *GetCPWeeklyRankStatusRequest, opts ...grpc.CallOption) (*GetCPWeeklyRankStatusResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cPWeeklyRankServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCPWeeklyRankServiceClient(cc grpc.ClientConnInterface) CPWeeklyRankServiceClient {
|
||||||
|
return &cPWeeklyRankServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cPWeeklyRankServiceClient) GetCPWeeklyRankStatus(ctx context.Context, in *GetCPWeeklyRankStatusRequest, opts ...grpc.CallOption) (*GetCPWeeklyRankStatusResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetCPWeeklyRankStatusResponse)
|
||||||
|
err := c.cc.Invoke(ctx, CPWeeklyRankService_GetCPWeeklyRankStatus_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPWeeklyRankServiceServer is the server API for CPWeeklyRankService service.
|
||||||
|
// All implementations must embed UnimplementedCPWeeklyRankServiceServer
|
||||||
|
// for forward compatibility.
|
||||||
|
//
|
||||||
|
// CPWeeklyRankService owns H5 reads for CP weekly rank and reward config.
|
||||||
|
type CPWeeklyRankServiceServer interface {
|
||||||
|
GetCPWeeklyRankStatus(context.Context, *GetCPWeeklyRankStatusRequest) (*GetCPWeeklyRankStatusResponse, error)
|
||||||
|
mustEmbedUnimplementedCPWeeklyRankServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedCPWeeklyRankServiceServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedCPWeeklyRankServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedCPWeeklyRankServiceServer) GetCPWeeklyRankStatus(context.Context, *GetCPWeeklyRankStatusRequest) (*GetCPWeeklyRankStatusResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetCPWeeklyRankStatus not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedCPWeeklyRankServiceServer) mustEmbedUnimplementedCPWeeklyRankServiceServer() {}
|
||||||
|
func (UnimplementedCPWeeklyRankServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeCPWeeklyRankServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to CPWeeklyRankServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeCPWeeklyRankServiceServer interface {
|
||||||
|
mustEmbedUnimplementedCPWeeklyRankServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterCPWeeklyRankServiceServer(s grpc.ServiceRegistrar, srv CPWeeklyRankServiceServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedCPWeeklyRankServiceServer 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.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&CPWeeklyRankService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _CPWeeklyRankService_GetCPWeeklyRankStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetCPWeeklyRankStatusRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(CPWeeklyRankServiceServer).GetCPWeeklyRankStatus(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: CPWeeklyRankService_GetCPWeeklyRankStatus_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(CPWeeklyRankServiceServer).GetCPWeeklyRankStatus(ctx, req.(*GetCPWeeklyRankStatusRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPWeeklyRankService_ServiceDesc is the grpc.ServiceDesc for CPWeeklyRankService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var CPWeeklyRankService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "hyapp.activity.v1.CPWeeklyRankService",
|
||||||
|
HandlerType: (*CPWeeklyRankServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "GetCPWeeklyRankStatus",
|
||||||
|
Handler: _CPWeeklyRankService_GetCPWeeklyRankStatus_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "proto/activity/v1/activity.proto",
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
AgencyOpeningService_GetAgencyOpeningStatus_FullMethodName = "/hyapp.activity.v1.AgencyOpeningService/GetAgencyOpeningStatus"
|
||||||
|
AgencyOpeningService_ApplyAgencyOpening_FullMethodName = "/hyapp.activity.v1.AgencyOpeningService/ApplyAgencyOpening"
|
||||||
|
AgencyOpeningService_ListAgencyOpeningLeaderboard_FullMethodName = "/hyapp.activity.v1.AgencyOpeningService/ListAgencyOpeningLeaderboard"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AgencyOpeningServiceClient is the client API for AgencyOpeningService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
//
|
||||||
|
// AgencyOpeningService owns App/H5 reads and application writes for agency opening activity.
|
||||||
|
type AgencyOpeningServiceClient interface {
|
||||||
|
GetAgencyOpeningStatus(ctx context.Context, in *GetAgencyOpeningStatusRequest, opts ...grpc.CallOption) (*GetAgencyOpeningStatusResponse, error)
|
||||||
|
ApplyAgencyOpening(ctx context.Context, in *ApplyAgencyOpeningRequest, opts ...grpc.CallOption) (*ApplyAgencyOpeningResponse, error)
|
||||||
|
ListAgencyOpeningLeaderboard(ctx context.Context, in *ListAgencyOpeningApplicationsRequest, opts ...grpc.CallOption) (*ListAgencyOpeningApplicationsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyOpeningServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAgencyOpeningServiceClient(cc grpc.ClientConnInterface) AgencyOpeningServiceClient {
|
||||||
|
return &agencyOpeningServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *agencyOpeningServiceClient) GetAgencyOpeningStatus(ctx context.Context, in *GetAgencyOpeningStatusRequest, opts ...grpc.CallOption) (*GetAgencyOpeningStatusResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetAgencyOpeningStatusResponse)
|
||||||
|
err := c.cc.Invoke(ctx, AgencyOpeningService_GetAgencyOpeningStatus_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *agencyOpeningServiceClient) ApplyAgencyOpening(ctx context.Context, in *ApplyAgencyOpeningRequest, opts ...grpc.CallOption) (*ApplyAgencyOpeningResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ApplyAgencyOpeningResponse)
|
||||||
|
err := c.cc.Invoke(ctx, AgencyOpeningService_ApplyAgencyOpening_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *agencyOpeningServiceClient) ListAgencyOpeningLeaderboard(ctx context.Context, in *ListAgencyOpeningApplicationsRequest, opts ...grpc.CallOption) (*ListAgencyOpeningApplicationsResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListAgencyOpeningApplicationsResponse)
|
||||||
|
err := c.cc.Invoke(ctx, AgencyOpeningService_ListAgencyOpeningLeaderboard_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgencyOpeningServiceServer is the server API for AgencyOpeningService service.
|
||||||
|
// All implementations must embed UnimplementedAgencyOpeningServiceServer
|
||||||
|
// for forward compatibility.
|
||||||
|
//
|
||||||
|
// AgencyOpeningService owns App/H5 reads and application writes for agency opening activity.
|
||||||
|
type AgencyOpeningServiceServer interface {
|
||||||
|
GetAgencyOpeningStatus(context.Context, *GetAgencyOpeningStatusRequest) (*GetAgencyOpeningStatusResponse, error)
|
||||||
|
ApplyAgencyOpening(context.Context, *ApplyAgencyOpeningRequest) (*ApplyAgencyOpeningResponse, error)
|
||||||
|
ListAgencyOpeningLeaderboard(context.Context, *ListAgencyOpeningApplicationsRequest) (*ListAgencyOpeningApplicationsResponse, error)
|
||||||
|
mustEmbedUnimplementedAgencyOpeningServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedAgencyOpeningServiceServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedAgencyOpeningServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedAgencyOpeningServiceServer) GetAgencyOpeningStatus(context.Context, *GetAgencyOpeningStatusRequest) (*GetAgencyOpeningStatusResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetAgencyOpeningStatus not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAgencyOpeningServiceServer) ApplyAgencyOpening(context.Context, *ApplyAgencyOpeningRequest) (*ApplyAgencyOpeningResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ApplyAgencyOpening not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAgencyOpeningServiceServer) ListAgencyOpeningLeaderboard(context.Context, *ListAgencyOpeningApplicationsRequest) (*ListAgencyOpeningApplicationsResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ListAgencyOpeningLeaderboard not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAgencyOpeningServiceServer) mustEmbedUnimplementedAgencyOpeningServiceServer() {}
|
||||||
|
func (UnimplementedAgencyOpeningServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeAgencyOpeningServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to AgencyOpeningServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeAgencyOpeningServiceServer interface {
|
||||||
|
mustEmbedUnimplementedAgencyOpeningServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterAgencyOpeningServiceServer(s grpc.ServiceRegistrar, srv AgencyOpeningServiceServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedAgencyOpeningServiceServer 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.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&AgencyOpeningService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _AgencyOpeningService_GetAgencyOpeningStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetAgencyOpeningStatusRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AgencyOpeningServiceServer).GetAgencyOpeningStatus(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: AgencyOpeningService_GetAgencyOpeningStatus_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AgencyOpeningServiceServer).GetAgencyOpeningStatus(ctx, req.(*GetAgencyOpeningStatusRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _AgencyOpeningService_ApplyAgencyOpening_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ApplyAgencyOpeningRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AgencyOpeningServiceServer).ApplyAgencyOpening(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: AgencyOpeningService_ApplyAgencyOpening_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AgencyOpeningServiceServer).ApplyAgencyOpening(ctx, req.(*ApplyAgencyOpeningRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _AgencyOpeningService_ListAgencyOpeningLeaderboard_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListAgencyOpeningApplicationsRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AgencyOpeningServiceServer).ListAgencyOpeningLeaderboard(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: AgencyOpeningService_ListAgencyOpeningLeaderboard_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AgencyOpeningServiceServer).ListAgencyOpeningLeaderboard(ctx, req.(*ListAgencyOpeningApplicationsRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgencyOpeningService_ServiceDesc is the grpc.ServiceDesc for AgencyOpeningService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var AgencyOpeningService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "hyapp.activity.v1.AgencyOpeningService",
|
||||||
|
HandlerType: (*AgencyOpeningServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "GetAgencyOpeningStatus",
|
||||||
|
Handler: _AgencyOpeningService_GetAgencyOpeningStatus_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ApplyAgencyOpening",
|
||||||
|
Handler: _AgencyOpeningService_ApplyAgencyOpening_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListAgencyOpeningLeaderboard",
|
||||||
|
Handler: _AgencyOpeningService_ListAgencyOpeningLeaderboard_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "proto/activity/v1/activity.proto",
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BroadcastService_EnsureBroadcastGroups_FullMethodName = "/hyapp.activity.v1.BroadcastService/EnsureBroadcastGroups"
|
BroadcastService_EnsureBroadcastGroups_FullMethodName = "/hyapp.activity.v1.BroadcastService/EnsureBroadcastGroups"
|
||||||
BroadcastService_PublishRegionBroadcast_FullMethodName = "/hyapp.activity.v1.BroadcastService/PublishRegionBroadcast"
|
BroadcastService_PublishRegionBroadcast_FullMethodName = "/hyapp.activity.v1.BroadcastService/PublishRegionBroadcast"
|
||||||
@ -4414,6 +4778,486 @@ var AdminWeeklyStarService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
Metadata: "proto/activity/v1/activity.proto",
|
Metadata: "proto/activity/v1/activity.proto",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
AdminCPWeeklyRankService_GetCPWeeklyRankConfig_FullMethodName = "/hyapp.activity.v1.AdminCPWeeklyRankService/GetCPWeeklyRankConfig"
|
||||||
|
AdminCPWeeklyRankService_UpdateCPWeeklyRankConfig_FullMethodName = "/hyapp.activity.v1.AdminCPWeeklyRankService/UpdateCPWeeklyRankConfig"
|
||||||
|
AdminCPWeeklyRankService_ListCPWeeklyRankSettlements_FullMethodName = "/hyapp.activity.v1.AdminCPWeeklyRankService/ListCPWeeklyRankSettlements"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminCPWeeklyRankServiceClient is the client API for AdminCPWeeklyRankService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
//
|
||||||
|
// AdminCPWeeklyRankService 是后台配置 CP 周榜启停、奖励和查询发奖事实的入口。
|
||||||
|
type AdminCPWeeklyRankServiceClient interface {
|
||||||
|
GetCPWeeklyRankConfig(ctx context.Context, in *GetCPWeeklyRankConfigRequest, opts ...grpc.CallOption) (*GetCPWeeklyRankConfigResponse, error)
|
||||||
|
UpdateCPWeeklyRankConfig(ctx context.Context, in *UpdateCPWeeklyRankConfigRequest, opts ...grpc.CallOption) (*UpdateCPWeeklyRankConfigResponse, error)
|
||||||
|
ListCPWeeklyRankSettlements(ctx context.Context, in *ListCPWeeklyRankSettlementsRequest, opts ...grpc.CallOption) (*ListCPWeeklyRankSettlementsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type adminCPWeeklyRankServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminCPWeeklyRankServiceClient(cc grpc.ClientConnInterface) AdminCPWeeklyRankServiceClient {
|
||||||
|
return &adminCPWeeklyRankServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adminCPWeeklyRankServiceClient) GetCPWeeklyRankConfig(ctx context.Context, in *GetCPWeeklyRankConfigRequest, opts ...grpc.CallOption) (*GetCPWeeklyRankConfigResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetCPWeeklyRankConfigResponse)
|
||||||
|
err := c.cc.Invoke(ctx, AdminCPWeeklyRankService_GetCPWeeklyRankConfig_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adminCPWeeklyRankServiceClient) UpdateCPWeeklyRankConfig(ctx context.Context, in *UpdateCPWeeklyRankConfigRequest, opts ...grpc.CallOption) (*UpdateCPWeeklyRankConfigResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UpdateCPWeeklyRankConfigResponse)
|
||||||
|
err := c.cc.Invoke(ctx, AdminCPWeeklyRankService_UpdateCPWeeklyRankConfig_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adminCPWeeklyRankServiceClient) ListCPWeeklyRankSettlements(ctx context.Context, in *ListCPWeeklyRankSettlementsRequest, opts ...grpc.CallOption) (*ListCPWeeklyRankSettlementsResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListCPWeeklyRankSettlementsResponse)
|
||||||
|
err := c.cc.Invoke(ctx, AdminCPWeeklyRankService_ListCPWeeklyRankSettlements_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminCPWeeklyRankServiceServer is the server API for AdminCPWeeklyRankService service.
|
||||||
|
// All implementations must embed UnimplementedAdminCPWeeklyRankServiceServer
|
||||||
|
// for forward compatibility.
|
||||||
|
//
|
||||||
|
// AdminCPWeeklyRankService 是后台配置 CP 周榜启停、奖励和查询发奖事实的入口。
|
||||||
|
type AdminCPWeeklyRankServiceServer interface {
|
||||||
|
GetCPWeeklyRankConfig(context.Context, *GetCPWeeklyRankConfigRequest) (*GetCPWeeklyRankConfigResponse, error)
|
||||||
|
UpdateCPWeeklyRankConfig(context.Context, *UpdateCPWeeklyRankConfigRequest) (*UpdateCPWeeklyRankConfigResponse, error)
|
||||||
|
ListCPWeeklyRankSettlements(context.Context, *ListCPWeeklyRankSettlementsRequest) (*ListCPWeeklyRankSettlementsResponse, error)
|
||||||
|
mustEmbedUnimplementedAdminCPWeeklyRankServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedAdminCPWeeklyRankServiceServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedAdminCPWeeklyRankServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedAdminCPWeeklyRankServiceServer) GetCPWeeklyRankConfig(context.Context, *GetCPWeeklyRankConfigRequest) (*GetCPWeeklyRankConfigResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetCPWeeklyRankConfig not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAdminCPWeeklyRankServiceServer) UpdateCPWeeklyRankConfig(context.Context, *UpdateCPWeeklyRankConfigRequest) (*UpdateCPWeeklyRankConfigResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method UpdateCPWeeklyRankConfig not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAdminCPWeeklyRankServiceServer) ListCPWeeklyRankSettlements(context.Context, *ListCPWeeklyRankSettlementsRequest) (*ListCPWeeklyRankSettlementsResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ListCPWeeklyRankSettlements not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAdminCPWeeklyRankServiceServer) mustEmbedUnimplementedAdminCPWeeklyRankServiceServer() {
|
||||||
|
}
|
||||||
|
func (UnimplementedAdminCPWeeklyRankServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeAdminCPWeeklyRankServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to AdminCPWeeklyRankServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeAdminCPWeeklyRankServiceServer interface {
|
||||||
|
mustEmbedUnimplementedAdminCPWeeklyRankServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterAdminCPWeeklyRankServiceServer(s grpc.ServiceRegistrar, srv AdminCPWeeklyRankServiceServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedAdminCPWeeklyRankServiceServer 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.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&AdminCPWeeklyRankService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _AdminCPWeeklyRankService_GetCPWeeklyRankConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetCPWeeklyRankConfigRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AdminCPWeeklyRankServiceServer).GetCPWeeklyRankConfig(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: AdminCPWeeklyRankService_GetCPWeeklyRankConfig_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AdminCPWeeklyRankServiceServer).GetCPWeeklyRankConfig(ctx, req.(*GetCPWeeklyRankConfigRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _AdminCPWeeklyRankService_UpdateCPWeeklyRankConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UpdateCPWeeklyRankConfigRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AdminCPWeeklyRankServiceServer).UpdateCPWeeklyRankConfig(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: AdminCPWeeklyRankService_UpdateCPWeeklyRankConfig_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AdminCPWeeklyRankServiceServer).UpdateCPWeeklyRankConfig(ctx, req.(*UpdateCPWeeklyRankConfigRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _AdminCPWeeklyRankService_ListCPWeeklyRankSettlements_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListCPWeeklyRankSettlementsRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AdminCPWeeklyRankServiceServer).ListCPWeeklyRankSettlements(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: AdminCPWeeklyRankService_ListCPWeeklyRankSettlements_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AdminCPWeeklyRankServiceServer).ListCPWeeklyRankSettlements(ctx, req.(*ListCPWeeklyRankSettlementsRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminCPWeeklyRankService_ServiceDesc is the grpc.ServiceDesc for AdminCPWeeklyRankService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var AdminCPWeeklyRankService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "hyapp.activity.v1.AdminCPWeeklyRankService",
|
||||||
|
HandlerType: (*AdminCPWeeklyRankServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "GetCPWeeklyRankConfig",
|
||||||
|
Handler: _AdminCPWeeklyRankService_GetCPWeeklyRankConfig_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "UpdateCPWeeklyRankConfig",
|
||||||
|
Handler: _AdminCPWeeklyRankService_UpdateCPWeeklyRankConfig_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListCPWeeklyRankSettlements",
|
||||||
|
Handler: _AdminCPWeeklyRankService_ListCPWeeklyRankSettlements_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "proto/activity/v1/activity.proto",
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
AdminAgencyOpeningService_ListAgencyOpeningCycles_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/ListAgencyOpeningCycles"
|
||||||
|
AdminAgencyOpeningService_CreateAgencyOpeningCycle_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/CreateAgencyOpeningCycle"
|
||||||
|
AdminAgencyOpeningService_GetAgencyOpeningCycle_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/GetAgencyOpeningCycle"
|
||||||
|
AdminAgencyOpeningService_UpdateAgencyOpeningCycle_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/UpdateAgencyOpeningCycle"
|
||||||
|
AdminAgencyOpeningService_SetAgencyOpeningCycleStatus_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/SetAgencyOpeningCycleStatus"
|
||||||
|
AdminAgencyOpeningService_ListAgencyOpeningApplications_FullMethodName = "/hyapp.activity.v1.AdminAgencyOpeningService/ListAgencyOpeningApplications"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminAgencyOpeningServiceClient is the client API for AdminAgencyOpeningService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
//
|
||||||
|
// AdminAgencyOpeningService 是后台配置开业周期、查看申请和榜单发奖结果的入口。
|
||||||
|
type AdminAgencyOpeningServiceClient interface {
|
||||||
|
ListAgencyOpeningCycles(ctx context.Context, in *ListAgencyOpeningCyclesRequest, opts ...grpc.CallOption) (*ListAgencyOpeningCyclesResponse, error)
|
||||||
|
CreateAgencyOpeningCycle(ctx context.Context, in *UpsertAgencyOpeningCycleRequest, opts ...grpc.CallOption) (*UpsertAgencyOpeningCycleResponse, error)
|
||||||
|
GetAgencyOpeningCycle(ctx context.Context, in *GetAgencyOpeningCycleRequest, opts ...grpc.CallOption) (*GetAgencyOpeningCycleResponse, error)
|
||||||
|
UpdateAgencyOpeningCycle(ctx context.Context, in *UpsertAgencyOpeningCycleRequest, opts ...grpc.CallOption) (*UpsertAgencyOpeningCycleResponse, error)
|
||||||
|
SetAgencyOpeningCycleStatus(ctx context.Context, in *SetAgencyOpeningCycleStatusRequest, opts ...grpc.CallOption) (*SetAgencyOpeningCycleStatusResponse, error)
|
||||||
|
ListAgencyOpeningApplications(ctx context.Context, in *ListAgencyOpeningApplicationsRequest, opts ...grpc.CallOption) (*ListAgencyOpeningApplicationsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type adminAgencyOpeningServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminAgencyOpeningServiceClient(cc grpc.ClientConnInterface) AdminAgencyOpeningServiceClient {
|
||||||
|
return &adminAgencyOpeningServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adminAgencyOpeningServiceClient) ListAgencyOpeningCycles(ctx context.Context, in *ListAgencyOpeningCyclesRequest, opts ...grpc.CallOption) (*ListAgencyOpeningCyclesResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListAgencyOpeningCyclesResponse)
|
||||||
|
err := c.cc.Invoke(ctx, AdminAgencyOpeningService_ListAgencyOpeningCycles_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adminAgencyOpeningServiceClient) CreateAgencyOpeningCycle(ctx context.Context, in *UpsertAgencyOpeningCycleRequest, opts ...grpc.CallOption) (*UpsertAgencyOpeningCycleResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UpsertAgencyOpeningCycleResponse)
|
||||||
|
err := c.cc.Invoke(ctx, AdminAgencyOpeningService_CreateAgencyOpeningCycle_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adminAgencyOpeningServiceClient) GetAgencyOpeningCycle(ctx context.Context, in *GetAgencyOpeningCycleRequest, opts ...grpc.CallOption) (*GetAgencyOpeningCycleResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetAgencyOpeningCycleResponse)
|
||||||
|
err := c.cc.Invoke(ctx, AdminAgencyOpeningService_GetAgencyOpeningCycle_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adminAgencyOpeningServiceClient) UpdateAgencyOpeningCycle(ctx context.Context, in *UpsertAgencyOpeningCycleRequest, opts ...grpc.CallOption) (*UpsertAgencyOpeningCycleResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UpsertAgencyOpeningCycleResponse)
|
||||||
|
err := c.cc.Invoke(ctx, AdminAgencyOpeningService_UpdateAgencyOpeningCycle_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adminAgencyOpeningServiceClient) SetAgencyOpeningCycleStatus(ctx context.Context, in *SetAgencyOpeningCycleStatusRequest, opts ...grpc.CallOption) (*SetAgencyOpeningCycleStatusResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(SetAgencyOpeningCycleStatusResponse)
|
||||||
|
err := c.cc.Invoke(ctx, AdminAgencyOpeningService_SetAgencyOpeningCycleStatus_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adminAgencyOpeningServiceClient) ListAgencyOpeningApplications(ctx context.Context, in *ListAgencyOpeningApplicationsRequest, opts ...grpc.CallOption) (*ListAgencyOpeningApplicationsResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListAgencyOpeningApplicationsResponse)
|
||||||
|
err := c.cc.Invoke(ctx, AdminAgencyOpeningService_ListAgencyOpeningApplications_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminAgencyOpeningServiceServer is the server API for AdminAgencyOpeningService service.
|
||||||
|
// All implementations must embed UnimplementedAdminAgencyOpeningServiceServer
|
||||||
|
// for forward compatibility.
|
||||||
|
//
|
||||||
|
// AdminAgencyOpeningService 是后台配置开业周期、查看申请和榜单发奖结果的入口。
|
||||||
|
type AdminAgencyOpeningServiceServer interface {
|
||||||
|
ListAgencyOpeningCycles(context.Context, *ListAgencyOpeningCyclesRequest) (*ListAgencyOpeningCyclesResponse, error)
|
||||||
|
CreateAgencyOpeningCycle(context.Context, *UpsertAgencyOpeningCycleRequest) (*UpsertAgencyOpeningCycleResponse, error)
|
||||||
|
GetAgencyOpeningCycle(context.Context, *GetAgencyOpeningCycleRequest) (*GetAgencyOpeningCycleResponse, error)
|
||||||
|
UpdateAgencyOpeningCycle(context.Context, *UpsertAgencyOpeningCycleRequest) (*UpsertAgencyOpeningCycleResponse, error)
|
||||||
|
SetAgencyOpeningCycleStatus(context.Context, *SetAgencyOpeningCycleStatusRequest) (*SetAgencyOpeningCycleStatusResponse, error)
|
||||||
|
ListAgencyOpeningApplications(context.Context, *ListAgencyOpeningApplicationsRequest) (*ListAgencyOpeningApplicationsResponse, error)
|
||||||
|
mustEmbedUnimplementedAdminAgencyOpeningServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedAdminAgencyOpeningServiceServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedAdminAgencyOpeningServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedAdminAgencyOpeningServiceServer) ListAgencyOpeningCycles(context.Context, *ListAgencyOpeningCyclesRequest) (*ListAgencyOpeningCyclesResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ListAgencyOpeningCycles not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAdminAgencyOpeningServiceServer) CreateAgencyOpeningCycle(context.Context, *UpsertAgencyOpeningCycleRequest) (*UpsertAgencyOpeningCycleResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method CreateAgencyOpeningCycle not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAdminAgencyOpeningServiceServer) GetAgencyOpeningCycle(context.Context, *GetAgencyOpeningCycleRequest) (*GetAgencyOpeningCycleResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetAgencyOpeningCycle not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAdminAgencyOpeningServiceServer) UpdateAgencyOpeningCycle(context.Context, *UpsertAgencyOpeningCycleRequest) (*UpsertAgencyOpeningCycleResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method UpdateAgencyOpeningCycle not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAdminAgencyOpeningServiceServer) SetAgencyOpeningCycleStatus(context.Context, *SetAgencyOpeningCycleStatusRequest) (*SetAgencyOpeningCycleStatusResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method SetAgencyOpeningCycleStatus not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAdminAgencyOpeningServiceServer) ListAgencyOpeningApplications(context.Context, *ListAgencyOpeningApplicationsRequest) (*ListAgencyOpeningApplicationsResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ListAgencyOpeningApplications not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedAdminAgencyOpeningServiceServer) mustEmbedUnimplementedAdminAgencyOpeningServiceServer() {
|
||||||
|
}
|
||||||
|
func (UnimplementedAdminAgencyOpeningServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeAdminAgencyOpeningServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to AdminAgencyOpeningServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeAdminAgencyOpeningServiceServer interface {
|
||||||
|
mustEmbedUnimplementedAdminAgencyOpeningServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterAdminAgencyOpeningServiceServer(s grpc.ServiceRegistrar, srv AdminAgencyOpeningServiceServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedAdminAgencyOpeningServiceServer 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.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&AdminAgencyOpeningService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _AdminAgencyOpeningService_ListAgencyOpeningCycles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListAgencyOpeningCyclesRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AdminAgencyOpeningServiceServer).ListAgencyOpeningCycles(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: AdminAgencyOpeningService_ListAgencyOpeningCycles_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AdminAgencyOpeningServiceServer).ListAgencyOpeningCycles(ctx, req.(*ListAgencyOpeningCyclesRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _AdminAgencyOpeningService_CreateAgencyOpeningCycle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UpsertAgencyOpeningCycleRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AdminAgencyOpeningServiceServer).CreateAgencyOpeningCycle(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: AdminAgencyOpeningService_CreateAgencyOpeningCycle_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AdminAgencyOpeningServiceServer).CreateAgencyOpeningCycle(ctx, req.(*UpsertAgencyOpeningCycleRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _AdminAgencyOpeningService_GetAgencyOpeningCycle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetAgencyOpeningCycleRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AdminAgencyOpeningServiceServer).GetAgencyOpeningCycle(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: AdminAgencyOpeningService_GetAgencyOpeningCycle_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AdminAgencyOpeningServiceServer).GetAgencyOpeningCycle(ctx, req.(*GetAgencyOpeningCycleRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _AdminAgencyOpeningService_UpdateAgencyOpeningCycle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UpsertAgencyOpeningCycleRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AdminAgencyOpeningServiceServer).UpdateAgencyOpeningCycle(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: AdminAgencyOpeningService_UpdateAgencyOpeningCycle_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AdminAgencyOpeningServiceServer).UpdateAgencyOpeningCycle(ctx, req.(*UpsertAgencyOpeningCycleRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _AdminAgencyOpeningService_SetAgencyOpeningCycleStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(SetAgencyOpeningCycleStatusRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AdminAgencyOpeningServiceServer).SetAgencyOpeningCycleStatus(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: AdminAgencyOpeningService_SetAgencyOpeningCycleStatus_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AdminAgencyOpeningServiceServer).SetAgencyOpeningCycleStatus(ctx, req.(*SetAgencyOpeningCycleStatusRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _AdminAgencyOpeningService_ListAgencyOpeningApplications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListAgencyOpeningApplicationsRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AdminAgencyOpeningServiceServer).ListAgencyOpeningApplications(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: AdminAgencyOpeningService_ListAgencyOpeningApplications_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(AdminAgencyOpeningServiceServer).ListAgencyOpeningApplications(ctx, req.(*ListAgencyOpeningApplicationsRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminAgencyOpeningService_ServiceDesc is the grpc.ServiceDesc for AdminAgencyOpeningService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var AdminAgencyOpeningService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "hyapp.activity.v1.AdminAgencyOpeningService",
|
||||||
|
HandlerType: (*AdminAgencyOpeningServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "ListAgencyOpeningCycles",
|
||||||
|
Handler: _AdminAgencyOpeningService_ListAgencyOpeningCycles_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CreateAgencyOpeningCycle",
|
||||||
|
Handler: _AdminAgencyOpeningService_CreateAgencyOpeningCycle_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetAgencyOpeningCycle",
|
||||||
|
Handler: _AdminAgencyOpeningService_GetAgencyOpeningCycle_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "UpdateAgencyOpeningCycle",
|
||||||
|
Handler: _AdminAgencyOpeningService_UpdateAgencyOpeningCycle_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "SetAgencyOpeningCycleStatus",
|
||||||
|
Handler: _AdminAgencyOpeningService_SetAgencyOpeningCycleStatus_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListAgencyOpeningApplications",
|
||||||
|
Handler: _AdminAgencyOpeningService_ListAgencyOpeningApplications_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "proto/activity/v1/activity.proto",
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SevenDayCheckInService_GetSevenDayCheckInStatus_FullMethodName = "/hyapp.activity.v1.SevenDayCheckInService/GetSevenDayCheckInStatus"
|
SevenDayCheckInService_GetSevenDayCheckInStatus_FullMethodName = "/hyapp.activity.v1.SevenDayCheckInService/GetSevenDayCheckInStatus"
|
||||||
SevenDayCheckInService_SignSevenDayCheckIn_FullMethodName = "/hyapp.activity.v1.SevenDayCheckInService/SignSevenDayCheckIn"
|
SevenDayCheckInService_SignSevenDayCheckIn_FullMethodName = "/hyapp.activity.v1.SevenDayCheckInService/SignSevenDayCheckIn"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.11
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v5.29.2
|
// protoc v7.35.0
|
||||||
// source: proto/events/room/v1/events.proto
|
// source: proto/events/room/v1/events.proto
|
||||||
|
|
||||||
package roomeventsv1
|
package roomeventsv1
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.11
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v5.29.2
|
// protoc v7.35.0
|
||||||
// source: proto/game/v1/game.proto
|
// source: proto/game/v1/game.proto
|
||||||
|
|
||||||
package gamev1
|
package gamev1
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.2
|
// - protoc-gen-go-grpc v1.6.2
|
||||||
// - protoc v5.29.2
|
// - protoc v7.35.0
|
||||||
// source: proto/game/v1/game.proto
|
// source: proto/game/v1/game.proto
|
||||||
|
|
||||||
package gamev1
|
package gamev1
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.11
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v5.29.2
|
// protoc v7.35.0
|
||||||
// source: proto/robot/v1/robot.proto
|
// source: proto/robot/v1/robot.proto
|
||||||
|
|
||||||
package robotv1
|
package robotv1
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.2
|
// - protoc-gen-go-grpc v1.6.2
|
||||||
// - protoc v5.29.2
|
// - protoc v7.35.0
|
||||||
// source: proto/robot/v1/robot.proto
|
// source: proto/robot/v1/robot.proto
|
||||||
|
|
||||||
package robotv1
|
package robotv1
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.11
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v5.29.2
|
// protoc v7.35.0
|
||||||
// source: proto/room/v1/room.proto
|
// source: proto/room/v1/room.proto
|
||||||
|
|
||||||
package roomv1
|
package roomv1
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.2
|
// - protoc-gen-go-grpc v1.6.2
|
||||||
// - protoc v5.29.2
|
// - protoc v7.35.0
|
||||||
// source: proto/room/v1/room.proto
|
// source: proto/room/v1/room.proto
|
||||||
|
|
||||||
package roomv1
|
package roomv1
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.11
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v5.29.2
|
// protoc v7.35.0
|
||||||
// source: proto/user/v1/auth.proto
|
// source: proto/user/v1/auth.proto
|
||||||
|
|
||||||
package userv1
|
package userv1
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.2
|
// - protoc-gen-go-grpc v1.6.2
|
||||||
// - protoc v5.29.2
|
// - protoc v7.35.0
|
||||||
// source: proto/user/v1/auth.proto
|
// source: proto/user/v1/auth.proto
|
||||||
|
|
||||||
package userv1
|
package userv1
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.11
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v5.29.2
|
// protoc v7.35.0
|
||||||
// source: proto/user/v1/host.proto
|
// source: proto/user/v1/host.proto
|
||||||
|
|
||||||
package userv1
|
package userv1
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.2
|
// - protoc-gen-go-grpc v1.6.2
|
||||||
// - protoc v5.29.2
|
// - protoc v7.35.0
|
||||||
// source: proto/user/v1/host.proto
|
// source: proto/user/v1/host.proto
|
||||||
|
|
||||||
package userv1
|
package userv1
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -85,6 +85,7 @@ message User {
|
|||||||
InviteOverview invite = 28;
|
InviteOverview invite = 28;
|
||||||
string pretty_id = 29;
|
string pretty_id = 29;
|
||||||
string pretty_display_user_id = 30;
|
string pretty_display_user_id = 30;
|
||||||
|
string profile_bg_img = 31;
|
||||||
}
|
}
|
||||||
|
|
||||||
// InviteOverview 是我的页可直接展示的邀请码和邀请计数 read model。
|
// InviteOverview 是我的页可直接展示的邀请码和邀请计数 read model。
|
||||||
@ -476,6 +477,33 @@ message CPIntimacyLeaderboardItem {
|
|||||||
int64 updated_at_ms = 9;
|
int64 updated_at_ms = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CPWeeklyRankEntry 是 activity-service 结算 CP 周榜时读取的关系周积分快照。
|
||||||
|
message CPWeeklyRankEntry {
|
||||||
|
int64 rank = 1;
|
||||||
|
string relationship_id = 2;
|
||||||
|
string relation_type = 3;
|
||||||
|
int64 score = 4;
|
||||||
|
CPIntimacyLeaderboardUser user_a = 5;
|
||||||
|
CPIntimacyLeaderboardUser user_b = 6;
|
||||||
|
int64 formed_at_ms = 7;
|
||||||
|
int64 updated_at_ms = 8;
|
||||||
|
int64 first_scored_at_ms = 9;
|
||||||
|
int64 last_scored_at_ms = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListCPWeeklyRankEntriesRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
string relation_type = 2;
|
||||||
|
int64 start_ms = 3;
|
||||||
|
int64 end_ms = 4;
|
||||||
|
int32 limit = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListCPWeeklyRankEntriesResponse {
|
||||||
|
repeated CPWeeklyRankEntry entries = 1;
|
||||||
|
int64 server_time_ms = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message ListCPApplicationsRequest {
|
message ListCPApplicationsRequest {
|
||||||
RequestMeta meta = 1;
|
RequestMeta meta = 1;
|
||||||
int64 user_id = 2;
|
int64 user_id = 2;
|
||||||
@ -693,6 +721,18 @@ message UpdateUserProfileResponse {
|
|||||||
User user = 1;
|
User user = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateUserProfileBackgroundRequest 只修改用户个人信息页背景图。
|
||||||
|
message UpdateUserProfileBackgroundRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
int64 user_id = 2;
|
||||||
|
string profile_bg_img = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserProfileBackgroundResponse 返回写入后的用户资料投影。
|
||||||
|
message UpdateUserProfileBackgroundResponse {
|
||||||
|
User user = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// ChangeUserCountryRequest 是 App 用户自助修改国家入口;受限业务身份不允许自助修改。
|
// ChangeUserCountryRequest 是 App 用户自助修改国家入口;受限业务身份不允许自助修改。
|
||||||
message ChangeUserCountryRequest {
|
message ChangeUserCountryRequest {
|
||||||
RequestMeta meta = 1;
|
RequestMeta meta = 1;
|
||||||
@ -1231,6 +1271,7 @@ service UserService {
|
|||||||
rpc ListUserIDs(ListUserIDsRequest) returns (ListUserIDsResponse);
|
rpc ListUserIDs(ListUserIDsRequest) returns (ListUserIDsResponse);
|
||||||
rpc GetUserMicLifetimeStats(GetUserMicLifetimeStatsRequest) returns (GetUserMicLifetimeStatsResponse);
|
rpc GetUserMicLifetimeStats(GetUserMicLifetimeStatsRequest) returns (GetUserMicLifetimeStatsResponse);
|
||||||
rpc UpdateUserProfile(UpdateUserProfileRequest) returns (UpdateUserProfileResponse);
|
rpc UpdateUserProfile(UpdateUserProfileRequest) returns (UpdateUserProfileResponse);
|
||||||
|
rpc UpdateUserProfileBackground(UpdateUserProfileBackgroundRequest) returns (UpdateUserProfileBackgroundResponse);
|
||||||
rpc ChangeUserCountry(ChangeUserCountryRequest) returns (ChangeUserCountryResponse);
|
rpc ChangeUserCountry(ChangeUserCountryRequest) returns (ChangeUserCountryResponse);
|
||||||
rpc AdminChangeUserCountry(AdminChangeUserCountryRequest) returns (ChangeUserCountryResponse);
|
rpc AdminChangeUserCountry(AdminChangeUserCountryRequest) returns (ChangeUserCountryResponse);
|
||||||
rpc SetUserStatus(SetUserStatusRequest) returns (SetUserStatusResponse);
|
rpc SetUserStatus(SetUserStatusRequest) returns (SetUserStatusResponse);
|
||||||
@ -1270,6 +1311,7 @@ service UserCPService {
|
|||||||
// UserCPInternalService 给 owner outbox 消费 worker 调用,不暴露给 App 或后台。
|
// UserCPInternalService 给 owner outbox 消费 worker 调用,不暴露给 App 或后台。
|
||||||
service UserCPInternalService {
|
service UserCPInternalService {
|
||||||
rpc ConsumeRoomGiftCPEvent(ConsumeRoomGiftCPEventRequest) returns (ConsumeRoomGiftCPEventResponse);
|
rpc ConsumeRoomGiftCPEvent(ConsumeRoomGiftCPEventRequest) returns (ConsumeRoomGiftCPEventResponse);
|
||||||
|
rpc ListCPWeeklyRankEntries(ListCPWeeklyRankEntriesRequest) returns (ListCPWeeklyRankEntriesResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserCronService 只给 cron-service 调用,业务状态仍由 user-service owner 修改。
|
// UserCronService 只给 cron-service 调用,业务状态仍由 user-service owner 修改。
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.2
|
// - protoc-gen-go-grpc v1.6.2
|
||||||
// - protoc v5.29.2
|
// - protoc v7.35.0
|
||||||
// source: proto/user/v1/user.proto
|
// source: proto/user/v1/user.proto
|
||||||
|
|
||||||
package userv1
|
package userv1
|
||||||
@ -19,21 +19,22 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UserService_GetUser_FullMethodName = "/hyapp.user.v1.UserService/GetUser"
|
UserService_GetUser_FullMethodName = "/hyapp.user.v1.UserService/GetUser"
|
||||||
UserService_GetInviteAttribution_FullMethodName = "/hyapp.user.v1.UserService/GetInviteAttribution"
|
UserService_GetInviteAttribution_FullMethodName = "/hyapp.user.v1.UserService/GetInviteAttribution"
|
||||||
UserService_BusinessUserLookup_FullMethodName = "/hyapp.user.v1.UserService/BusinessUserLookup"
|
UserService_BusinessUserLookup_FullMethodName = "/hyapp.user.v1.UserService/BusinessUserLookup"
|
||||||
UserService_GetMyProfileStats_FullMethodName = "/hyapp.user.v1.UserService/GetMyProfileStats"
|
UserService_GetMyProfileStats_FullMethodName = "/hyapp.user.v1.UserService/GetMyProfileStats"
|
||||||
UserService_BatchGetUsers_FullMethodName = "/hyapp.user.v1.UserService/BatchGetUsers"
|
UserService_BatchGetUsers_FullMethodName = "/hyapp.user.v1.UserService/BatchGetUsers"
|
||||||
UserService_ListUserIDs_FullMethodName = "/hyapp.user.v1.UserService/ListUserIDs"
|
UserService_ListUserIDs_FullMethodName = "/hyapp.user.v1.UserService/ListUserIDs"
|
||||||
UserService_GetUserMicLifetimeStats_FullMethodName = "/hyapp.user.v1.UserService/GetUserMicLifetimeStats"
|
UserService_GetUserMicLifetimeStats_FullMethodName = "/hyapp.user.v1.UserService/GetUserMicLifetimeStats"
|
||||||
UserService_UpdateUserProfile_FullMethodName = "/hyapp.user.v1.UserService/UpdateUserProfile"
|
UserService_UpdateUserProfile_FullMethodName = "/hyapp.user.v1.UserService/UpdateUserProfile"
|
||||||
UserService_ChangeUserCountry_FullMethodName = "/hyapp.user.v1.UserService/ChangeUserCountry"
|
UserService_UpdateUserProfileBackground_FullMethodName = "/hyapp.user.v1.UserService/UpdateUserProfileBackground"
|
||||||
UserService_AdminChangeUserCountry_FullMethodName = "/hyapp.user.v1.UserService/AdminChangeUserCountry"
|
UserService_ChangeUserCountry_FullMethodName = "/hyapp.user.v1.UserService/ChangeUserCountry"
|
||||||
UserService_SetUserStatus_FullMethodName = "/hyapp.user.v1.UserService/SetUserStatus"
|
UserService_AdminChangeUserCountry_FullMethodName = "/hyapp.user.v1.UserService/AdminChangeUserCountry"
|
||||||
UserService_CreateManagerUserBlock_FullMethodName = "/hyapp.user.v1.UserService/CreateManagerUserBlock"
|
UserService_SetUserStatus_FullMethodName = "/hyapp.user.v1.UserService/SetUserStatus"
|
||||||
UserService_ListManagerUserBlocks_FullMethodName = "/hyapp.user.v1.UserService/ListManagerUserBlocks"
|
UserService_CreateManagerUserBlock_FullMethodName = "/hyapp.user.v1.UserService/CreateManagerUserBlock"
|
||||||
UserService_UnblockManagerUser_FullMethodName = "/hyapp.user.v1.UserService/UnblockManagerUser"
|
UserService_ListManagerUserBlocks_FullMethodName = "/hyapp.user.v1.UserService/ListManagerUserBlocks"
|
||||||
UserService_CompleteOnboarding_FullMethodName = "/hyapp.user.v1.UserService/CompleteOnboarding"
|
UserService_UnblockManagerUser_FullMethodName = "/hyapp.user.v1.UserService/UnblockManagerUser"
|
||||||
|
UserService_CompleteOnboarding_FullMethodName = "/hyapp.user.v1.UserService/CompleteOnboarding"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserServiceClient is the client API for UserService service.
|
// UserServiceClient is the client API for UserService service.
|
||||||
@ -50,6 +51,7 @@ type UserServiceClient interface {
|
|||||||
ListUserIDs(ctx context.Context, in *ListUserIDsRequest, opts ...grpc.CallOption) (*ListUserIDsResponse, error)
|
ListUserIDs(ctx context.Context, in *ListUserIDsRequest, opts ...grpc.CallOption) (*ListUserIDsResponse, error)
|
||||||
GetUserMicLifetimeStats(ctx context.Context, in *GetUserMicLifetimeStatsRequest, opts ...grpc.CallOption) (*GetUserMicLifetimeStatsResponse, error)
|
GetUserMicLifetimeStats(ctx context.Context, in *GetUserMicLifetimeStatsRequest, opts ...grpc.CallOption) (*GetUserMicLifetimeStatsResponse, error)
|
||||||
UpdateUserProfile(ctx context.Context, in *UpdateUserProfileRequest, opts ...grpc.CallOption) (*UpdateUserProfileResponse, error)
|
UpdateUserProfile(ctx context.Context, in *UpdateUserProfileRequest, opts ...grpc.CallOption) (*UpdateUserProfileResponse, error)
|
||||||
|
UpdateUserProfileBackground(ctx context.Context, in *UpdateUserProfileBackgroundRequest, opts ...grpc.CallOption) (*UpdateUserProfileBackgroundResponse, error)
|
||||||
ChangeUserCountry(ctx context.Context, in *ChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, error)
|
ChangeUserCountry(ctx context.Context, in *ChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, error)
|
||||||
AdminChangeUserCountry(ctx context.Context, in *AdminChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, error)
|
AdminChangeUserCountry(ctx context.Context, in *AdminChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, error)
|
||||||
SetUserStatus(ctx context.Context, in *SetUserStatusRequest, opts ...grpc.CallOption) (*SetUserStatusResponse, error)
|
SetUserStatus(ctx context.Context, in *SetUserStatusRequest, opts ...grpc.CallOption) (*SetUserStatusResponse, error)
|
||||||
@ -147,6 +149,16 @@ func (c *userServiceClient) UpdateUserProfile(ctx context.Context, in *UpdateUse
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *userServiceClient) UpdateUserProfileBackground(ctx context.Context, in *UpdateUserProfileBackgroundRequest, opts ...grpc.CallOption) (*UpdateUserProfileBackgroundResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UpdateUserProfileBackgroundResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserService_UpdateUserProfileBackground_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *userServiceClient) ChangeUserCountry(ctx context.Context, in *ChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, error) {
|
func (c *userServiceClient) ChangeUserCountry(ctx context.Context, in *ChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(ChangeUserCountryResponse)
|
out := new(ChangeUserCountryResponse)
|
||||||
@ -231,6 +243,7 @@ type UserServiceServer interface {
|
|||||||
ListUserIDs(context.Context, *ListUserIDsRequest) (*ListUserIDsResponse, error)
|
ListUserIDs(context.Context, *ListUserIDsRequest) (*ListUserIDsResponse, error)
|
||||||
GetUserMicLifetimeStats(context.Context, *GetUserMicLifetimeStatsRequest) (*GetUserMicLifetimeStatsResponse, error)
|
GetUserMicLifetimeStats(context.Context, *GetUserMicLifetimeStatsRequest) (*GetUserMicLifetimeStatsResponse, error)
|
||||||
UpdateUserProfile(context.Context, *UpdateUserProfileRequest) (*UpdateUserProfileResponse, error)
|
UpdateUserProfile(context.Context, *UpdateUserProfileRequest) (*UpdateUserProfileResponse, error)
|
||||||
|
UpdateUserProfileBackground(context.Context, *UpdateUserProfileBackgroundRequest) (*UpdateUserProfileBackgroundResponse, error)
|
||||||
ChangeUserCountry(context.Context, *ChangeUserCountryRequest) (*ChangeUserCountryResponse, error)
|
ChangeUserCountry(context.Context, *ChangeUserCountryRequest) (*ChangeUserCountryResponse, error)
|
||||||
AdminChangeUserCountry(context.Context, *AdminChangeUserCountryRequest) (*ChangeUserCountryResponse, error)
|
AdminChangeUserCountry(context.Context, *AdminChangeUserCountryRequest) (*ChangeUserCountryResponse, error)
|
||||||
SetUserStatus(context.Context, *SetUserStatusRequest) (*SetUserStatusResponse, error)
|
SetUserStatus(context.Context, *SetUserStatusRequest) (*SetUserStatusResponse, error)
|
||||||
@ -272,6 +285,9 @@ func (UnimplementedUserServiceServer) GetUserMicLifetimeStats(context.Context, *
|
|||||||
func (UnimplementedUserServiceServer) UpdateUserProfile(context.Context, *UpdateUserProfileRequest) (*UpdateUserProfileResponse, error) {
|
func (UnimplementedUserServiceServer) UpdateUserProfile(context.Context, *UpdateUserProfileRequest) (*UpdateUserProfileResponse, error) {
|
||||||
return nil, status.Error(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.Error(codes.Unimplemented, "method UpdateUserProfileBackground not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedUserServiceServer) ChangeUserCountry(context.Context, *ChangeUserCountryRequest) (*ChangeUserCountryResponse, error) {
|
func (UnimplementedUserServiceServer) ChangeUserCountry(context.Context, *ChangeUserCountryRequest) (*ChangeUserCountryResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method ChangeUserCountry not implemented")
|
return nil, status.Error(codes.Unimplemented, "method ChangeUserCountry not implemented")
|
||||||
}
|
}
|
||||||
@ -458,6 +474,24 @@ func _UserService_UpdateUserProfile_Handler(srv interface{}, ctx context.Context
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _UserService_UpdateUserProfileBackground_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UpdateUserProfileBackgroundRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserServiceServer).UpdateUserProfileBackground(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserService_UpdateUserProfileBackground_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserServiceServer).UpdateUserProfileBackground(ctx, req.(*UpdateUserProfileBackgroundRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _UserService_ChangeUserCountry_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _UserService_ChangeUserCountry_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ChangeUserCountryRequest)
|
in := new(ChangeUserCountryRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -623,6 +657,10 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "UpdateUserProfile",
|
MethodName: "UpdateUserProfile",
|
||||||
Handler: _UserService_UpdateUserProfile_Handler,
|
Handler: _UserService_UpdateUserProfile_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "UpdateUserProfileBackground",
|
||||||
|
Handler: _UserService_UpdateUserProfileBackground_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "ChangeUserCountry",
|
MethodName: "ChangeUserCountry",
|
||||||
Handler: _UserService_ChangeUserCountry_Handler,
|
Handler: _UserService_ChangeUserCountry_Handler,
|
||||||
@ -1515,7 +1553,8 @@ var UserCPService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UserCPInternalService_ConsumeRoomGiftCPEvent_FullMethodName = "/hyapp.user.v1.UserCPInternalService/ConsumeRoomGiftCPEvent"
|
UserCPInternalService_ConsumeRoomGiftCPEvent_FullMethodName = "/hyapp.user.v1.UserCPInternalService/ConsumeRoomGiftCPEvent"
|
||||||
|
UserCPInternalService_ListCPWeeklyRankEntries_FullMethodName = "/hyapp.user.v1.UserCPInternalService/ListCPWeeklyRankEntries"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserCPInternalServiceClient is the client API for UserCPInternalService service.
|
// UserCPInternalServiceClient is the client API for UserCPInternalService service.
|
||||||
@ -1525,6 +1564,7 @@ const (
|
|||||||
// UserCPInternalService 给 owner outbox 消费 worker 调用,不暴露给 App 或后台。
|
// UserCPInternalService 给 owner outbox 消费 worker 调用,不暴露给 App 或后台。
|
||||||
type UserCPInternalServiceClient interface {
|
type UserCPInternalServiceClient interface {
|
||||||
ConsumeRoomGiftCPEvent(ctx context.Context, in *ConsumeRoomGiftCPEventRequest, opts ...grpc.CallOption) (*ConsumeRoomGiftCPEventResponse, error)
|
ConsumeRoomGiftCPEvent(ctx context.Context, in *ConsumeRoomGiftCPEventRequest, opts ...grpc.CallOption) (*ConsumeRoomGiftCPEventResponse, error)
|
||||||
|
ListCPWeeklyRankEntries(ctx context.Context, in *ListCPWeeklyRankEntriesRequest, opts ...grpc.CallOption) (*ListCPWeeklyRankEntriesResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type userCPInternalServiceClient struct {
|
type userCPInternalServiceClient struct {
|
||||||
@ -1545,6 +1585,16 @@ func (c *userCPInternalServiceClient) ConsumeRoomGiftCPEvent(ctx context.Context
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *userCPInternalServiceClient) ListCPWeeklyRankEntries(ctx context.Context, in *ListCPWeeklyRankEntriesRequest, opts ...grpc.CallOption) (*ListCPWeeklyRankEntriesResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListCPWeeklyRankEntriesResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserCPInternalService_ListCPWeeklyRankEntries_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UserCPInternalServiceServer is the server API for UserCPInternalService service.
|
// UserCPInternalServiceServer is the server API for UserCPInternalService service.
|
||||||
// All implementations must embed UnimplementedUserCPInternalServiceServer
|
// All implementations must embed UnimplementedUserCPInternalServiceServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@ -1552,6 +1602,7 @@ func (c *userCPInternalServiceClient) ConsumeRoomGiftCPEvent(ctx context.Context
|
|||||||
// UserCPInternalService 给 owner outbox 消费 worker 调用,不暴露给 App 或后台。
|
// UserCPInternalService 给 owner outbox 消费 worker 调用,不暴露给 App 或后台。
|
||||||
type UserCPInternalServiceServer interface {
|
type UserCPInternalServiceServer interface {
|
||||||
ConsumeRoomGiftCPEvent(context.Context, *ConsumeRoomGiftCPEventRequest) (*ConsumeRoomGiftCPEventResponse, error)
|
ConsumeRoomGiftCPEvent(context.Context, *ConsumeRoomGiftCPEventRequest) (*ConsumeRoomGiftCPEventResponse, error)
|
||||||
|
ListCPWeeklyRankEntries(context.Context, *ListCPWeeklyRankEntriesRequest) (*ListCPWeeklyRankEntriesResponse, error)
|
||||||
mustEmbedUnimplementedUserCPInternalServiceServer()
|
mustEmbedUnimplementedUserCPInternalServiceServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1565,6 +1616,9 @@ type UnimplementedUserCPInternalServiceServer struct{}
|
|||||||
func (UnimplementedUserCPInternalServiceServer) ConsumeRoomGiftCPEvent(context.Context, *ConsumeRoomGiftCPEventRequest) (*ConsumeRoomGiftCPEventResponse, error) {
|
func (UnimplementedUserCPInternalServiceServer) ConsumeRoomGiftCPEvent(context.Context, *ConsumeRoomGiftCPEventRequest) (*ConsumeRoomGiftCPEventResponse, error) {
|
||||||
return nil, status.Error(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.Error(codes.Unimplemented, "method ListCPWeeklyRankEntries not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedUserCPInternalServiceServer) mustEmbedUnimplementedUserCPInternalServiceServer() {}
|
func (UnimplementedUserCPInternalServiceServer) mustEmbedUnimplementedUserCPInternalServiceServer() {}
|
||||||
func (UnimplementedUserCPInternalServiceServer) testEmbeddedByValue() {}
|
func (UnimplementedUserCPInternalServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@ -1604,6 +1658,24 @@ func _UserCPInternalService_ConsumeRoomGiftCPEvent_Handler(srv interface{}, ctx
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _UserCPInternalService_ListCPWeeklyRankEntries_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListCPWeeklyRankEntriesRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserCPInternalServiceServer).ListCPWeeklyRankEntries(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserCPInternalService_ListCPWeeklyRankEntries_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserCPInternalServiceServer).ListCPWeeklyRankEntries(ctx, req.(*ListCPWeeklyRankEntriesRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// UserCPInternalService_ServiceDesc is the grpc.ServiceDesc for UserCPInternalService service.
|
// UserCPInternalService_ServiceDesc is the grpc.ServiceDesc for UserCPInternalService service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@ -1615,6 +1687,10 @@ var UserCPInternalService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ConsumeRoomGiftCPEvent",
|
MethodName: "ConsumeRoomGiftCPEvent",
|
||||||
Handler: _UserCPInternalService_ConsumeRoomGiftCPEvent_Handler,
|
Handler: _UserCPInternalService_ConsumeRoomGiftCPEvent_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListCPWeeklyRankEntries",
|
||||||
|
Handler: _UserCPInternalService_ListCPWeeklyRankEntries_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "proto/user/v1/user.proto",
|
Metadata: "proto/user/v1/user.proto",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1610,6 +1610,28 @@ message CreditInviteActivityRewardResponse {
|
|||||||
int64 granted_at_ms = 4;
|
int64 granted_at_ms = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreditAgencyOpeningRewardRequest 是 activity-service 代理开业流水档位结算后的 COIN 入账命令。
|
||||||
|
message CreditAgencyOpeningRewardRequest {
|
||||||
|
string command_id = 1;
|
||||||
|
string app_code = 2;
|
||||||
|
int64 target_user_id = 3;
|
||||||
|
int64 amount = 4;
|
||||||
|
string application_id = 5;
|
||||||
|
string cycle_id = 6;
|
||||||
|
int64 agency_id = 7;
|
||||||
|
int32 rank_no = 8;
|
||||||
|
int64 score_coins = 9;
|
||||||
|
string reason = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreditAgencyOpeningRewardResponse 返回代理开业奖励入账流水和用户 COIN 账后余额。
|
||||||
|
message CreditAgencyOpeningRewardResponse {
|
||||||
|
string transaction_id = 1;
|
||||||
|
AssetBalance balance = 2;
|
||||||
|
int64 amount = 3;
|
||||||
|
int64 granted_at_ms = 4;
|
||||||
|
}
|
||||||
|
|
||||||
// ApplyGameCoinChangeRequest 是 game-service 唯一可以调用的钱包游戏改账入口。
|
// ApplyGameCoinChangeRequest 是 game-service 唯一可以调用的钱包游戏改账入口。
|
||||||
message ApplyGameCoinChangeRequest {
|
message ApplyGameCoinChangeRequest {
|
||||||
string request_id = 1;
|
string request_id = 1;
|
||||||
@ -1913,6 +1935,7 @@ service WalletService {
|
|||||||
rpc CreditLuckyGiftReward(CreditLuckyGiftRewardRequest) returns (CreditLuckyGiftRewardResponse);
|
rpc CreditLuckyGiftReward(CreditLuckyGiftRewardRequest) returns (CreditLuckyGiftRewardResponse);
|
||||||
rpc CreditRoomTurnoverReward(CreditRoomTurnoverRewardRequest) returns (CreditRoomTurnoverRewardResponse);
|
rpc CreditRoomTurnoverReward(CreditRoomTurnoverRewardRequest) returns (CreditRoomTurnoverRewardResponse);
|
||||||
rpc CreditInviteActivityReward(CreditInviteActivityRewardRequest) returns (CreditInviteActivityRewardResponse);
|
rpc CreditInviteActivityReward(CreditInviteActivityRewardRequest) returns (CreditInviteActivityRewardResponse);
|
||||||
|
rpc CreditAgencyOpeningReward(CreditAgencyOpeningRewardRequest) returns (CreditAgencyOpeningRewardResponse);
|
||||||
rpc ApplyGameCoinChange(ApplyGameCoinChangeRequest) returns (ApplyGameCoinChangeResponse);
|
rpc ApplyGameCoinChange(ApplyGameCoinChangeRequest) returns (ApplyGameCoinChangeResponse);
|
||||||
rpc GetRedPacketConfig(GetRedPacketConfigRequest) returns (GetRedPacketConfigResponse);
|
rpc GetRedPacketConfig(GetRedPacketConfigRequest) returns (GetRedPacketConfigResponse);
|
||||||
rpc UpdateRedPacketConfig(UpdateRedPacketConfigRequest) returns (UpdateRedPacketConfigResponse);
|
rpc UpdateRedPacketConfig(UpdateRedPacketConfigRequest) returns (UpdateRedPacketConfigResponse);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.2
|
// - protoc-gen-go-grpc v1.6.2
|
||||||
// - protoc v5.29.2
|
// - protoc v7.35.0
|
||||||
// source: proto/wallet/v1/wallet.proto
|
// source: proto/wallet/v1/wallet.proto
|
||||||
|
|
||||||
package walletv1
|
package walletv1
|
||||||
@ -272,6 +272,7 @@ const (
|
|||||||
WalletService_CreditLuckyGiftReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditLuckyGiftReward"
|
WalletService_CreditLuckyGiftReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditLuckyGiftReward"
|
||||||
WalletService_CreditRoomTurnoverReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditRoomTurnoverReward"
|
WalletService_CreditRoomTurnoverReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditRoomTurnoverReward"
|
||||||
WalletService_CreditInviteActivityReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditInviteActivityReward"
|
WalletService_CreditInviteActivityReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditInviteActivityReward"
|
||||||
|
WalletService_CreditAgencyOpeningReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditAgencyOpeningReward"
|
||||||
WalletService_ApplyGameCoinChange_FullMethodName = "/hyapp.wallet.v1.WalletService/ApplyGameCoinChange"
|
WalletService_ApplyGameCoinChange_FullMethodName = "/hyapp.wallet.v1.WalletService/ApplyGameCoinChange"
|
||||||
WalletService_GetRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRedPacketConfig"
|
WalletService_GetRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRedPacketConfig"
|
||||||
WalletService_UpdateRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateRedPacketConfig"
|
WalletService_UpdateRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateRedPacketConfig"
|
||||||
@ -360,6 +361,7 @@ type WalletServiceClient interface {
|
|||||||
CreditLuckyGiftReward(ctx context.Context, in *CreditLuckyGiftRewardRequest, opts ...grpc.CallOption) (*CreditLuckyGiftRewardResponse, error)
|
CreditLuckyGiftReward(ctx context.Context, in *CreditLuckyGiftRewardRequest, opts ...grpc.CallOption) (*CreditLuckyGiftRewardResponse, error)
|
||||||
CreditRoomTurnoverReward(ctx context.Context, in *CreditRoomTurnoverRewardRequest, opts ...grpc.CallOption) (*CreditRoomTurnoverRewardResponse, error)
|
CreditRoomTurnoverReward(ctx context.Context, in *CreditRoomTurnoverRewardRequest, opts ...grpc.CallOption) (*CreditRoomTurnoverRewardResponse, error)
|
||||||
CreditInviteActivityReward(ctx context.Context, in *CreditInviteActivityRewardRequest, opts ...grpc.CallOption) (*CreditInviteActivityRewardResponse, error)
|
CreditInviteActivityReward(ctx context.Context, in *CreditInviteActivityRewardRequest, opts ...grpc.CallOption) (*CreditInviteActivityRewardResponse, error)
|
||||||
|
CreditAgencyOpeningReward(ctx context.Context, in *CreditAgencyOpeningRewardRequest, opts ...grpc.CallOption) (*CreditAgencyOpeningRewardResponse, error)
|
||||||
ApplyGameCoinChange(ctx context.Context, in *ApplyGameCoinChangeRequest, opts ...grpc.CallOption) (*ApplyGameCoinChangeResponse, error)
|
ApplyGameCoinChange(ctx context.Context, in *ApplyGameCoinChangeRequest, opts ...grpc.CallOption) (*ApplyGameCoinChangeResponse, error)
|
||||||
GetRedPacketConfig(ctx context.Context, in *GetRedPacketConfigRequest, opts ...grpc.CallOption) (*GetRedPacketConfigResponse, error)
|
GetRedPacketConfig(ctx context.Context, in *GetRedPacketConfigRequest, opts ...grpc.CallOption) (*GetRedPacketConfigResponse, error)
|
||||||
UpdateRedPacketConfig(ctx context.Context, in *UpdateRedPacketConfigRequest, opts ...grpc.CallOption) (*UpdateRedPacketConfigResponse, error)
|
UpdateRedPacketConfig(ctx context.Context, in *UpdateRedPacketConfigRequest, opts ...grpc.CallOption) (*UpdateRedPacketConfigResponse, error)
|
||||||
@ -1089,6 +1091,16 @@ func (c *walletServiceClient) CreditInviteActivityReward(ctx context.Context, in
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *walletServiceClient) CreditAgencyOpeningReward(ctx context.Context, in *CreditAgencyOpeningRewardRequest, opts ...grpc.CallOption) (*CreditAgencyOpeningRewardResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CreditAgencyOpeningRewardResponse)
|
||||||
|
err := c.cc.Invoke(ctx, WalletService_CreditAgencyOpeningReward_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *walletServiceClient) ApplyGameCoinChange(ctx context.Context, in *ApplyGameCoinChangeRequest, opts ...grpc.CallOption) (*ApplyGameCoinChangeResponse, error) {
|
func (c *walletServiceClient) ApplyGameCoinChange(ctx context.Context, in *ApplyGameCoinChangeRequest, opts ...grpc.CallOption) (*ApplyGameCoinChangeResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(ApplyGameCoinChangeResponse)
|
out := new(ApplyGameCoinChangeResponse)
|
||||||
@ -1256,6 +1268,7 @@ type WalletServiceServer interface {
|
|||||||
CreditLuckyGiftReward(context.Context, *CreditLuckyGiftRewardRequest) (*CreditLuckyGiftRewardResponse, error)
|
CreditLuckyGiftReward(context.Context, *CreditLuckyGiftRewardRequest) (*CreditLuckyGiftRewardResponse, error)
|
||||||
CreditRoomTurnoverReward(context.Context, *CreditRoomTurnoverRewardRequest) (*CreditRoomTurnoverRewardResponse, error)
|
CreditRoomTurnoverReward(context.Context, *CreditRoomTurnoverRewardRequest) (*CreditRoomTurnoverRewardResponse, error)
|
||||||
CreditInviteActivityReward(context.Context, *CreditInviteActivityRewardRequest) (*CreditInviteActivityRewardResponse, error)
|
CreditInviteActivityReward(context.Context, *CreditInviteActivityRewardRequest) (*CreditInviteActivityRewardResponse, error)
|
||||||
|
CreditAgencyOpeningReward(context.Context, *CreditAgencyOpeningRewardRequest) (*CreditAgencyOpeningRewardResponse, error)
|
||||||
ApplyGameCoinChange(context.Context, *ApplyGameCoinChangeRequest) (*ApplyGameCoinChangeResponse, error)
|
ApplyGameCoinChange(context.Context, *ApplyGameCoinChangeRequest) (*ApplyGameCoinChangeResponse, error)
|
||||||
GetRedPacketConfig(context.Context, *GetRedPacketConfigRequest) (*GetRedPacketConfigResponse, error)
|
GetRedPacketConfig(context.Context, *GetRedPacketConfigRequest) (*GetRedPacketConfigResponse, error)
|
||||||
UpdateRedPacketConfig(context.Context, *UpdateRedPacketConfigRequest) (*UpdateRedPacketConfigResponse, error)
|
UpdateRedPacketConfig(context.Context, *UpdateRedPacketConfigRequest) (*UpdateRedPacketConfigResponse, error)
|
||||||
@ -1488,6 +1501,9 @@ func (UnimplementedWalletServiceServer) CreditRoomTurnoverReward(context.Context
|
|||||||
func (UnimplementedWalletServiceServer) CreditInviteActivityReward(context.Context, *CreditInviteActivityRewardRequest) (*CreditInviteActivityRewardResponse, error) {
|
func (UnimplementedWalletServiceServer) CreditInviteActivityReward(context.Context, *CreditInviteActivityRewardRequest) (*CreditInviteActivityRewardResponse, error) {
|
||||||
return nil, status.Error(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.Error(codes.Unimplemented, "method CreditAgencyOpeningReward not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedWalletServiceServer) ApplyGameCoinChange(context.Context, *ApplyGameCoinChangeRequest) (*ApplyGameCoinChangeResponse, error) {
|
func (UnimplementedWalletServiceServer) ApplyGameCoinChange(context.Context, *ApplyGameCoinChangeRequest) (*ApplyGameCoinChangeResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method ApplyGameCoinChange not implemented")
|
return nil, status.Error(codes.Unimplemented, "method ApplyGameCoinChange not implemented")
|
||||||
}
|
}
|
||||||
@ -2814,6 +2830,24 @@ func _WalletService_CreditInviteActivityReward_Handler(srv interface{}, ctx cont
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _WalletService_CreditAgencyOpeningReward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CreditAgencyOpeningRewardRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(WalletServiceServer).CreditAgencyOpeningReward(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: WalletService_CreditAgencyOpeningReward_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(WalletServiceServer).CreditAgencyOpeningReward(ctx, req.(*CreditAgencyOpeningRewardRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _WalletService_ApplyGameCoinChange_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _WalletService_ApplyGameCoinChange_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ApplyGameCoinChangeRequest)
|
in := new(ApplyGameCoinChangeRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -3267,6 +3301,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "CreditInviteActivityReward",
|
MethodName: "CreditInviteActivityReward",
|
||||||
Handler: _WalletService_CreditInviteActivityReward_Handler,
|
Handler: _WalletService_CreditInviteActivityReward_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CreditAgencyOpeningReward",
|
||||||
|
Handler: _WalletService_CreditAgencyOpeningReward_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "ApplyGameCoinChange",
|
MethodName: "ApplyGameCoinChange",
|
||||||
Handler: _WalletService_ApplyGameCoinChange_Handler,
|
Handler: _WalletService_ApplyGameCoinChange_Handler,
|
||||||
|
|||||||
103
docs/flutter对接/个人主页背景图Flutter对接.md
Normal file
103
docs/flutter对接/个人主页背景图Flutter对接.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
# 个人主页背景图 Flutter 对接
|
||||||
|
|
||||||
|
个人主页背景图是用户资料字段 `profile_bg_img`,不是背包资源,不是 `profile_card`,也不需要佩戴。App 先自行上传图片拿到 URL,再调用后端保存 URL。
|
||||||
|
|
||||||
|
## 保存主页背景图
|
||||||
|
|
||||||
|
地址:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/users/me/profile-bg-img
|
||||||
|
```
|
||||||
|
|
||||||
|
Headers:
|
||||||
|
|
||||||
|
| Header | 必填 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Authorization` | 是 | `Bearer <access_token>` |
|
||||||
|
| `Content-Type` | 是 | `application/json` |
|
||||||
|
| `X-App-Code` | 否 | App 编码,默认 `lalu` |
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `url` | string | 是 | App 已上传好的图片 URL,必须是 `http` 或 `https` 绝对地址。 |
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"url": "https://cdn.example.com/profile-bg.png"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "OK",
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": "req_abc",
|
||||||
|
"data": {
|
||||||
|
"profile_bg_img": "https://cdn.example.com/profile-bg.png",
|
||||||
|
"profile": {
|
||||||
|
"user_id": "10001",
|
||||||
|
"display_user_id": "163001",
|
||||||
|
"username": "Alice",
|
||||||
|
"avatar": "https://cdn.example.com/avatar.png",
|
||||||
|
"profile_bg_img": "https://cdn.example.com/profile-bg.png",
|
||||||
|
"profile_completed": true,
|
||||||
|
"onboarding_status": "completed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Flutter 处理:
|
||||||
|
|
||||||
|
| 场景 | 处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| 保存成功 | 使用 `data.profile_bg_img` 刷新当前资料页背景图。 |
|
||||||
|
| 重新打开我的资料页 | 读取个人信息接口返回的 `profile_bg_img`。 |
|
||||||
|
| 查看别人资料卡 | 使用访问资料卡响应里的 `target_profile.profile_bg_img`。 |
|
||||||
|
|
||||||
|
## 个人信息返回字段
|
||||||
|
|
||||||
|
当前用户个人信息:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/users/me
|
||||||
|
```
|
||||||
|
|
||||||
|
我的页概览:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/users/me/overview
|
||||||
|
```
|
||||||
|
|
||||||
|
访问别人的资料卡:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/users/{user_id}/visit
|
||||||
|
```
|
||||||
|
|
||||||
|
返回字段:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `profile_bg_img` | 用户主页背景图 URL;未设置时为空字符串。 |
|
||||||
|
| `profile.profile_bg_img` | `/users/me/overview` 里的用户资料背景图。 |
|
||||||
|
| `target_profile.profile_bg_img` | 查看别人资料卡时,对方的主页背景图。 |
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
| HTTP | code | 场景 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `400` | `INVALID_ARGUMENT` | `url` 为空、不是 HTTP(S) 绝对地址或长度超过限制。 |
|
||||||
|
| `401` | `UNAUTHORIZED` | 未登录或 token 无效。 |
|
||||||
|
| `403` | `PROFILE_REQUIRED` | 当前用户资料未完成。 |
|
||||||
|
| `404` | `NOT_FOUND` | 当前用户不存在。 |
|
||||||
|
| `502` | `UPSTREAM_ERROR` | user-service 不可用。 |
|
||||||
|
|
||||||
|
相关 IM:无。该接口只保存用户资料字段,不发送 IM,不触发背包或装扮刷新。
|
||||||
@ -225,6 +225,7 @@ Body:
|
|||||||
| 权益校验 | 必须是当前登录用户自己的有效权益;过期、禁用、剩余数量为 0 都不能佩戴。 |
|
| 权益校验 | 必须是当前登录用户自己的有效权益;过期、禁用、剩余数量为 0 都不能佩戴。 |
|
||||||
| 发放默认佩戴 | `badge`、`mic_seat_animation` 发放成功后默认佩戴。 |
|
| 发放默认佩戴 | `badge`、`mic_seat_animation` 发放成功后默认佩戴。 |
|
||||||
| 徽章展示 | `badge` 是拥有即佩戴的多装备资源;资料页多徽章展示仍使用 `/api/v1/badges/me` 和 `/api/v1/badges/display`。 |
|
| 徽章展示 | `badge` 是拥有即佩戴的多装备资源;资料页多徽章展示仍使用 `/api/v1/badges/me` 和 `/api/v1/badges/display`。 |
|
||||||
|
| 资料卡背景 | `profile_card` 只用于装扮资料卡;个人主页背景图使用用户资料字段 `profile_bg_img`,不走背包佩戴。 |
|
||||||
|
|
||||||
常见错误:
|
常见错误:
|
||||||
|
|
||||||
|
|||||||
@ -866,7 +866,7 @@ paths:
|
|||||||
- users
|
- users
|
||||||
summary: 修改当前用户基础资料
|
summary: 修改当前用户基础资料
|
||||||
operationId: updateMyProfile
|
operationId: updateMyProfile
|
||||||
description: 只修改 `username`、`avatar`、`birth`;国家修改必须走独立国家接口。
|
description: 只修改 `username`、`avatar`、`birth`;主页背景图和国家修改都走独立接口。
|
||||||
security:
|
security:
|
||||||
- BearerAuth: []
|
- BearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -890,6 +890,36 @@ paths:
|
|||||||
$ref: "#/responses/Internal"
|
$ref: "#/responses/Internal"
|
||||||
"502":
|
"502":
|
||||||
$ref: "#/responses/UpstreamError"
|
$ref: "#/responses/UpstreamError"
|
||||||
|
/api/v1/users/me/profile-bg-img:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
summary: 修改当前用户主页背景图
|
||||||
|
operationId: updateMyProfileBackground
|
||||||
|
description: App 先自行上传图片,本接口只接收图片 URL 并保存到 `profile_bg_img`,和背包、`profile_card`、佩戴无关。
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: body
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/UpdateUserProfileBackgroundRequest"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 修改成功,`data.profile_bg_img` 返回最新背景图 URL。
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/UpdateUserProfileBackgroundEnvelope"
|
||||||
|
"400":
|
||||||
|
$ref: "#/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "#/responses/Unauthorized"
|
||||||
|
"404":
|
||||||
|
$ref: "#/responses/NotFound"
|
||||||
|
"500":
|
||||||
|
$ref: "#/responses/Internal"
|
||||||
|
"502":
|
||||||
|
$ref: "#/responses/UpstreamError"
|
||||||
/api/v1/users/me/country/change:
|
/api/v1/users/me/country/change:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@ -3144,6 +3174,9 @@ definitions:
|
|||||||
updated_at_ms:
|
updated_at_ms:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
|
profile_bg_img:
|
||||||
|
type: string
|
||||||
|
description: 用户主页信息卡背景图片 URL,来自 users.profile_bg_img;和 `profile_card` 装扮资源无关,未设置时为空字符串。
|
||||||
next_country_change_allowed_at_ms:
|
next_country_change_allowed_at_ms:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
@ -3594,6 +3627,28 @@ definitions:
|
|||||||
birth:
|
birth:
|
||||||
type: string
|
type: string
|
||||||
description: yyyy-mm-dd;传空字符串表示清空生日。
|
description: yyyy-mm-dd;传空字符串表示清空生日。
|
||||||
|
UpdateUserProfileBackgroundRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- url
|
||||||
|
properties:
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
description: App 已上传好的个人主页背景图 URL。
|
||||||
|
UpdateUserProfileBackgroundData:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
profile_bg_img:
|
||||||
|
type: string
|
||||||
|
profile:
|
||||||
|
$ref: "#/definitions/UserProfileData"
|
||||||
|
UpdateUserProfileBackgroundEnvelope:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: "#/definitions/UpdateUserProfileBackgroundData"
|
||||||
ChangeUserCountryRequest:
|
ChangeUserCountryRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@ -927,6 +927,9 @@ definitions:
|
|||||||
description: 用户选择的国家主数据 country_code。
|
description: 用户选择的国家主数据 country_code。
|
||||||
avatar:
|
avatar:
|
||||||
type: string
|
type: string
|
||||||
|
profile_bg_img:
|
||||||
|
type: string
|
||||||
|
description: 用户主页信息卡背景图片 URL,属于用户资料字段;和 `profile_card` 装扮资源无关。
|
||||||
birth:
|
birth:
|
||||||
type: string
|
type: string
|
||||||
description: yyyy-mm-dd。
|
description: yyyy-mm-dd。
|
||||||
@ -1085,6 +1088,22 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
user:
|
user:
|
||||||
$ref: "#/definitions/User"
|
$ref: "#/definitions/User"
|
||||||
|
UpdateUserProfileBackgroundRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
meta:
|
||||||
|
$ref: "#/definitions/RequestMeta"
|
||||||
|
user_id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
profile_bg_img:
|
||||||
|
type: string
|
||||||
|
description: 用户主页背景图 URL。
|
||||||
|
UpdateUserProfileBackgroundResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
user:
|
||||||
|
$ref: "#/definitions/User"
|
||||||
CompleteOnboardingRequest:
|
CompleteOnboardingRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@ -27,7 +27,8 @@ Authorization: Bearer <access_token>
|
|||||||
"avatar": "https://cdn.example.com/avatar.png",
|
"avatar": "https://cdn.example.com/avatar.png",
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"profile_completed": true,
|
"profile_completed": true,
|
||||||
"onboarding_status": "completed"
|
"onboarding_status": "completed",
|
||||||
|
"profile_bg_img": "https://cdn.example.com/profile-bg.png"
|
||||||
},
|
},
|
||||||
"profile_stats": {
|
"profile_stats": {
|
||||||
"visitors_count": 12,
|
"visitors_count": 12,
|
||||||
@ -97,6 +98,7 @@ Authorization: Bearer <access_token>
|
|||||||
| Field | Source | Rule |
|
| Field | Source | Rule |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `profile` | `user-service.GetUser` | 只取头像、昵称、展示 ID、语言等基础资料 |
|
| `profile` | `user-service.GetUser` | 只取头像、昵称、展示 ID、语言等基础资料 |
|
||||||
|
| `profile.profile_bg_img` | `user-service.GetUser` | 取用户资料里的主页背景图;和 `profile_card` 装扮资源无关,未设置返回空字符串 |
|
||||||
| `profile_stats` | `user-service.GetMyProfileStats` | 读 `user_profile_stats` 计数表,不实时扫访问、关注、好友大表 |
|
| `profile_stats` | `user-service.GetMyProfileStats` | 读 `user_profile_stats` 计数表,不实时扫访问、关注、好友大表 |
|
||||||
| `wallet` | `wallet-service.GetWalletValueSummary` | 首屏只返回金币余额,不返回钻石和美元余额 |
|
| `wallet` | `wallet-service.GetWalletValueSummary` | 首屏只返回金币余额,不返回钻石和美元余额 |
|
||||||
| `vip` | `wallet-service.GetMyVip` | 只取等级、名称、有效期和 active 状态 |
|
| `vip` | `wallet-service.GetMyVip` | 只取等级、名称、有效期和 active 状态 |
|
||||||
|
|||||||
@ -523,5 +523,6 @@ Flutter 处理:
|
|||||||
2. 背包页调 `/api/v1/users/me/resources?resource_type=xxx`。
|
2. 背包页调 `/api/v1/users/me/resources?resource_type=xxx`。
|
||||||
3. 点击佩戴调 `POST /api/v1/users/me/resources/{resource_id}/equip`,成功后刷新 `/appearance`。
|
3. 点击佩戴调 `POST /api/v1/users/me/resources/{resource_id}/equip`,成功后刷新 `/appearance`。
|
||||||
4. 点击取消佩戴调 `DELETE /api/v1/users/me/resources/{resource_type}/unequip`,成功后刷新 `/appearance`。
|
4. 点击取消佩戴调 `DELETE /api/v1/users/me/resources/{resource_type}/unequip`,成功后刷新 `/appearance`。
|
||||||
5. 房间、公屏、在线用户资料优先使用 `room-display-profiles:batch` 的 `avatar_frame`、`profile_card`、`vehicle`、`badges`。
|
5. 个人主页信息卡背景使用个人资料返回的 `profile_bg_img`;该字段是用户资料字段,和 `profile_card` 装扮资源无关。
|
||||||
6. 收到 `room_user_joined` IM 时,只按 `entry_vehicle` 播放入场座驾。
|
6. 房间、公屏、在线用户资料优先使用 `room-display-profiles:batch` 的 `avatar_frame`、`profile_card`、`vehicle`、`badges`。
|
||||||
|
7. 收到 `room_user_joined` IM 时,只按 `entry_vehicle` 播放入场座驾。
|
||||||
|
|||||||
@ -52,6 +52,27 @@ sequenceDiagram
|
|||||||
|
|
||||||
## Response Examples
|
## Response Examples
|
||||||
|
|
||||||
|
访问资料卡:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"recorded": true,
|
||||||
|
"target_stats": {
|
||||||
|
"visitors_count": 1,
|
||||||
|
"following_count": 0,
|
||||||
|
"friends_count": 0,
|
||||||
|
"updated_at_ms": 1777996800000
|
||||||
|
},
|
||||||
|
"target_profile": {
|
||||||
|
"user_id": "10002",
|
||||||
|
"display_user_id": "163002",
|
||||||
|
"username": "Bob",
|
||||||
|
"avatar": "https://cdn.example.com/avatar.png",
|
||||||
|
"profile_bg_img": "https://cdn.example.com/profile-bg.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
申请好友:
|
申请好友:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@ -87,6 +87,8 @@ go run ./cmd/server -config configs/config.yaml -bootstrap
|
|||||||
7. 国家创建和国家启用/禁用是后台管理后端能力,实现在 `hyapp-admin-server`,不要为这些后台动作新增 user-service RPC 或 user-service 业务逻辑。
|
7. 国家创建和国家启用/禁用是后台管理后端能力,实现在 `hyapp-admin-server`,不要为这些后台动作新增 user-service RPC 或 user-service 业务逻辑。
|
||||||
8. 后台创建 BD Leader 按目标用户当前 `users.region_id` 创建;admin-server 只负责权限和审计,不传 `regionId`,user-service 在关系创建事务里读取目标用户区域并写入 `bd_leader_profiles.region_id`,不要在创建 BD Leader 时顺手修改用户区域。
|
8. 后台创建 BD Leader 按目标用户当前 `users.region_id` 创建;admin-server 只负责权限和审计,不传 `regionId`,user-service 在关系创建事务里读取目标用户区域并写入 `bd_leader_profiles.region_id`,不要在创建 BD Leader 时顺手修改用户区域。
|
||||||
9. `hyapp.local/api` 是唯一允许直接依赖的 app 后端契约 module;不要 import `hyapp/services/*`、`hyapp/internal/*` 或 `hyapp/pkg/*`。
|
9. `hyapp.local/api` 是唯一允许直接依赖的 app 后端契约 module;不要 import `hyapp/services/*`、`hyapp/internal/*` 或 `hyapp/pkg/*`。
|
||||||
|
10. 运营管理/全服通知只能调用 `activityv1.MessageInboxService.CreateFanoutJob` 创建异步 fanout 命令;admin-server 不直接写 activity-service 数据表,也不能在 HTTP 请求里同步遍历用户发系统/活动消息。
|
||||||
|
11. 全服通知菜单 code 固定为 `operation-full-server-notice`,权限固定为 `full-server-notice:view` 和 `full-server-notice:send`;新增 UI、OpenAPI、种子权限和路由时必须保持这些 code 一致。
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
|
|||||||
@ -64,6 +64,27 @@ go run ./cmd/server -config configs/config.yaml -bootstrap
|
|||||||
- 数据权限通过角色的 `data-scopes` 保存,并在后台用户列表/导出查询中执行。
|
- 数据权限通过角色的 `data-scopes` 保存,并在后台用户列表/导出查询中执行。
|
||||||
- 任务产物默认写入 `storage/exports`,通过 `/api/v1/jobs/:id/artifact` 下载。
|
- 任务产物默认写入 `storage/exports`,通过 `/api/v1/jobs/:id/artifact` 下载。
|
||||||
|
|
||||||
|
## Full Server Notice
|
||||||
|
|
||||||
|
菜单入口:运营管理 / 全服通知,菜单 code 为 `operation-full-server-notice`,页面路径为 `/operations/full-server-notices`。
|
||||||
|
|
||||||
|
接口:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST /api/v1/admin/operations/full-server-notices/fanout
|
||||||
|
```
|
||||||
|
|
||||||
|
参数只传后台需要的命令字段:`message_type` 为 `system` 或 `activity`,`target_scope` 支持 `all_active_users`、`single_user`、`user_ids`、`region`、`country`,内容字段为 `title`、`summary`、`body`,跳转字段为 `action_type`、`action_param`,扩展字段为 `metadata_json`。`command_id` 是幂等键,不传时由 admin-server 按管理员和当前时间生成。
|
||||||
|
|
||||||
|
返回值包含 `job_id`、`status`、`created`、`command_id`、`message_type`、`target_scope`。接口只创建 activity-service 的 `message_fanout_jobs`,实际逐用户写入由 message fanout worker 分批完成。
|
||||||
|
|
||||||
|
权限:
|
||||||
|
|
||||||
|
- 查看菜单:`full-server-notice:view`
|
||||||
|
- 发送通知:`full-server-notice:send`
|
||||||
|
|
||||||
|
相关 IM:Flutter 消息页的系统/活动标签读取后端 inbox;当前客户端只展示标题、摘要/正文和时间。腾讯云 IM 单聊和房间群消息不走这个后台入口。
|
||||||
|
|
||||||
## Verify
|
## Verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import (
|
|||||||
"hyapp-admin-server/internal/migration"
|
"hyapp-admin-server/internal/migration"
|
||||||
achievementconfigmodule "hyapp-admin-server/internal/modules/achievementconfig"
|
achievementconfigmodule "hyapp-admin-server/internal/modules/achievementconfig"
|
||||||
adminusermodule "hyapp-admin-server/internal/modules/adminuser"
|
adminusermodule "hyapp-admin-server/internal/modules/adminuser"
|
||||||
|
agencyopeningmodule "hyapp-admin-server/internal/modules/agencyopening"
|
||||||
appconfigmodule "hyapp-admin-server/internal/modules/appconfig"
|
appconfigmodule "hyapp-admin-server/internal/modules/appconfig"
|
||||||
appregistrymodule "hyapp-admin-server/internal/modules/appregistry"
|
appregistrymodule "hyapp-admin-server/internal/modules/appregistry"
|
||||||
appusermodule "hyapp-admin-server/internal/modules/appuser"
|
appusermodule "hyapp-admin-server/internal/modules/appuser"
|
||||||
@ -38,6 +39,7 @@ import (
|
|||||||
dailytaskmodule "hyapp-admin-server/internal/modules/dailytask"
|
dailytaskmodule "hyapp-admin-server/internal/modules/dailytask"
|
||||||
dashboardmodule "hyapp-admin-server/internal/modules/dashboard"
|
dashboardmodule "hyapp-admin-server/internal/modules/dashboard"
|
||||||
firstrechargerewardmodule "hyapp-admin-server/internal/modules/firstrechargereward"
|
firstrechargerewardmodule "hyapp-admin-server/internal/modules/firstrechargereward"
|
||||||
|
fullservernoticemodule "hyapp-admin-server/internal/modules/fullservernotice"
|
||||||
gamemanagementmodule "hyapp-admin-server/internal/modules/gamemanagement"
|
gamemanagementmodule "hyapp-admin-server/internal/modules/gamemanagement"
|
||||||
giftdiamondmodule "hyapp-admin-server/internal/modules/giftdiamond"
|
giftdiamondmodule "hyapp-admin-server/internal/modules/giftdiamond"
|
||||||
healthmodule "hyapp-admin-server/internal/modules/health"
|
healthmodule "hyapp-admin-server/internal/modules/health"
|
||||||
@ -252,17 +254,19 @@ func main() {
|
|||||||
Audit: auditHandler,
|
Audit: auditHandler,
|
||||||
Auth: authmodule.New(store, auth, auditHandler, cfg),
|
Auth: authmodule.New(store, auth, auditHandler, cfg),
|
||||||
AdminUser: adminusermodule.New(store, cfg, auditHandler),
|
AdminUser: adminusermodule.New(store, cfg, auditHandler),
|
||||||
|
AgencyOpening: agencyopeningmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||||
AchievementConfig: achievementconfigmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
AchievementConfig: achievementconfigmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||||
AppConfig: appconfigmodule.New(store, auditHandler),
|
AppConfig: appconfigmodule.New(store, auditHandler),
|
||||||
AppRegistry: appregistrymodule.New(userDB),
|
AppRegistry: appregistrymodule.New(userDB),
|
||||||
AppUser: appusermodule.New(userclient.NewGRPC(userConn), activityclient.NewGRPC(activityConn), sqlDB, userDB, walletDB, cfg, auditHandler),
|
AppUser: appusermodule.New(userclient.NewGRPC(userConn), activityclient.NewGRPC(activityConn), sqlDB, userDB, walletDB, cfg, auditHandler),
|
||||||
CoinLedger: coinledgermodule.New(userDB, walletDB, sqlDB, walletclient.NewGRPC(walletConn), auditHandler),
|
CoinLedger: coinledgermodule.New(userDB, walletDB, sqlDB, walletclient.NewGRPC(walletConn), auditHandler),
|
||||||
CountryRegion: countryregionmodule.New(userclient.NewGRPC(userConn), userDB, cfg, auditHandler),
|
CountryRegion: countryregionmodule.New(userclient.NewGRPC(userConn), userDB, cfg, auditHandler),
|
||||||
CPRelation: cprelationmodule.New(userDB, auditHandler),
|
CPRelation: cprelationmodule.NewWithActivity(userDB, activityclient.NewGRPC(activityConn), auditHandler),
|
||||||
CumulativeRecharge: cumulativerechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
CumulativeRecharge: cumulativerechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||||
DailyTask: dailytaskmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
DailyTask: dailytaskmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||||
Dashboard: dashboardmodule.New(store, cfg, userclient.NewGRPC(userConn)),
|
Dashboard: dashboardmodule.New(store, cfg, userclient.NewGRPC(userConn)),
|
||||||
FirstRechargeReward: firstrechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
FirstRechargeReward: firstrechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||||
|
FullServerNotice: fullservernoticemodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||||
Game: gamemanagementmodule.New(
|
Game: gamemanagementmodule.New(
|
||||||
gameclient.NewGRPC(gameConn),
|
gameclient.NewGRPC(gameConn),
|
||||||
userclient.NewGRPC(userConn),
|
userclient.NewGRPC(userConn),
|
||||||
|
|||||||
@ -42,6 +42,15 @@ type Client interface {
|
|||||||
SetWeeklyStarCycleStatus(ctx context.Context, req *activityv1.SetWeeklyStarCycleStatusRequest) (*activityv1.SetWeeklyStarCycleStatusResponse, error)
|
SetWeeklyStarCycleStatus(ctx context.Context, req *activityv1.SetWeeklyStarCycleStatusRequest) (*activityv1.SetWeeklyStarCycleStatusResponse, error)
|
||||||
ListWeeklyStarLeaderboard(ctx context.Context, req *activityv1.ListWeeklyStarLeaderboardRequest) (*activityv1.ListWeeklyStarLeaderboardResponse, error)
|
ListWeeklyStarLeaderboard(ctx context.Context, req *activityv1.ListWeeklyStarLeaderboardRequest) (*activityv1.ListWeeklyStarLeaderboardResponse, error)
|
||||||
ListWeeklyStarSettlements(ctx context.Context, req *activityv1.ListWeeklyStarSettlementsRequest) (*activityv1.ListWeeklyStarSettlementsResponse, error)
|
ListWeeklyStarSettlements(ctx context.Context, req *activityv1.ListWeeklyStarSettlementsRequest) (*activityv1.ListWeeklyStarSettlementsResponse, error)
|
||||||
|
ListAgencyOpeningCycles(ctx context.Context, req *activityv1.ListAgencyOpeningCyclesRequest) (*activityv1.ListAgencyOpeningCyclesResponse, error)
|
||||||
|
CreateAgencyOpeningCycle(ctx context.Context, req *activityv1.UpsertAgencyOpeningCycleRequest) (*activityv1.UpsertAgencyOpeningCycleResponse, error)
|
||||||
|
GetAgencyOpeningCycle(ctx context.Context, req *activityv1.GetAgencyOpeningCycleRequest) (*activityv1.GetAgencyOpeningCycleResponse, error)
|
||||||
|
UpdateAgencyOpeningCycle(ctx context.Context, req *activityv1.UpsertAgencyOpeningCycleRequest) (*activityv1.UpsertAgencyOpeningCycleResponse, error)
|
||||||
|
SetAgencyOpeningCycleStatus(ctx context.Context, req *activityv1.SetAgencyOpeningCycleStatusRequest) (*activityv1.SetAgencyOpeningCycleStatusResponse, error)
|
||||||
|
ListAgencyOpeningApplications(ctx context.Context, req *activityv1.ListAgencyOpeningApplicationsRequest) (*activityv1.ListAgencyOpeningApplicationsResponse, 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)
|
||||||
GetLuckyGiftConfig(ctx context.Context, req *activityv1.GetLuckyGiftConfigRequest) (*activityv1.GetLuckyGiftConfigResponse, error)
|
GetLuckyGiftConfig(ctx context.Context, req *activityv1.GetLuckyGiftConfigRequest) (*activityv1.GetLuckyGiftConfigResponse, error)
|
||||||
UpsertLuckyGiftConfig(ctx context.Context, req *activityv1.UpsertLuckyGiftConfigRequest) (*activityv1.UpsertLuckyGiftConfigResponse, error)
|
UpsertLuckyGiftConfig(ctx context.Context, req *activityv1.UpsertLuckyGiftConfigRequest) (*activityv1.UpsertLuckyGiftConfigResponse, error)
|
||||||
ListLuckyGiftConfigs(ctx context.Context, req *activityv1.ListLuckyGiftConfigsRequest) (*activityv1.ListLuckyGiftConfigsResponse, error)
|
ListLuckyGiftConfigs(ctx context.Context, req *activityv1.ListLuckyGiftConfigsRequest) (*activityv1.ListLuckyGiftConfigsResponse, error)
|
||||||
@ -52,6 +61,8 @@ type Client interface {
|
|||||||
UpsertLevelTrack(ctx context.Context, req *activityv1.UpsertLevelTrackRequest) (*activityv1.UpsertLevelTrackResponse, error)
|
UpsertLevelTrack(ctx context.Context, req *activityv1.UpsertLevelTrackRequest) (*activityv1.UpsertLevelTrackResponse, error)
|
||||||
UpsertLevelRule(ctx context.Context, req *activityv1.UpsertLevelRuleRequest) (*activityv1.UpsertLevelRuleResponse, error)
|
UpsertLevelRule(ctx context.Context, req *activityv1.UpsertLevelRuleRequest) (*activityv1.UpsertLevelRuleResponse, error)
|
||||||
UpsertLevelTier(ctx context.Context, req *activityv1.UpsertLevelTierRequest) (*activityv1.UpsertLevelTierResponse, error)
|
UpsertLevelTier(ctx context.Context, req *activityv1.UpsertLevelTierRequest) (*activityv1.UpsertLevelTierResponse, error)
|
||||||
|
CreateInboxMessage(ctx context.Context, req *activityv1.CreateInboxMessageRequest) (*activityv1.CreateInboxMessageResponse, error)
|
||||||
|
CreateFanoutJob(ctx context.Context, req *activityv1.CreateFanoutJobRequest) (*activityv1.CreateFanoutJobResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GRPCClient struct {
|
type GRPCClient struct {
|
||||||
@ -64,9 +75,12 @@ type GRPCClient struct {
|
|||||||
sevenDayCheckInClient activityv1.AdminSevenDayCheckInServiceClient
|
sevenDayCheckInClient activityv1.AdminSevenDayCheckInServiceClient
|
||||||
roomTurnoverRewardClient activityv1.AdminRoomTurnoverRewardServiceClient
|
roomTurnoverRewardClient activityv1.AdminRoomTurnoverRewardServiceClient
|
||||||
weeklyStarClient activityv1.AdminWeeklyStarServiceClient
|
weeklyStarClient activityv1.AdminWeeklyStarServiceClient
|
||||||
|
agencyOpeningClient activityv1.AdminAgencyOpeningServiceClient
|
||||||
|
cpWeeklyRankClient activityv1.AdminCPWeeklyRankServiceClient
|
||||||
luckyGiftClient activityv1.AdminLuckyGiftServiceClient
|
luckyGiftClient activityv1.AdminLuckyGiftServiceClient
|
||||||
broadcastClient activityv1.BroadcastServiceClient
|
broadcastClient activityv1.BroadcastServiceClient
|
||||||
growthClient activityv1.AdminGrowthLevelServiceClient
|
growthClient activityv1.AdminGrowthLevelServiceClient
|
||||||
|
messageClient activityv1.MessageInboxServiceClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
|
func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
|
||||||
@ -80,9 +94,12 @@ func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
|
|||||||
sevenDayCheckInClient: activityv1.NewAdminSevenDayCheckInServiceClient(conn),
|
sevenDayCheckInClient: activityv1.NewAdminSevenDayCheckInServiceClient(conn),
|
||||||
roomTurnoverRewardClient: activityv1.NewAdminRoomTurnoverRewardServiceClient(conn),
|
roomTurnoverRewardClient: activityv1.NewAdminRoomTurnoverRewardServiceClient(conn),
|
||||||
weeklyStarClient: activityv1.NewAdminWeeklyStarServiceClient(conn),
|
weeklyStarClient: activityv1.NewAdminWeeklyStarServiceClient(conn),
|
||||||
|
agencyOpeningClient: activityv1.NewAdminAgencyOpeningServiceClient(conn),
|
||||||
|
cpWeeklyRankClient: activityv1.NewAdminCPWeeklyRankServiceClient(conn),
|
||||||
luckyGiftClient: activityv1.NewAdminLuckyGiftServiceClient(conn),
|
luckyGiftClient: activityv1.NewAdminLuckyGiftServiceClient(conn),
|
||||||
broadcastClient: activityv1.NewBroadcastServiceClient(conn),
|
broadcastClient: activityv1.NewBroadcastServiceClient(conn),
|
||||||
growthClient: activityv1.NewAdminGrowthLevelServiceClient(conn),
|
growthClient: activityv1.NewAdminGrowthLevelServiceClient(conn),
|
||||||
|
messageClient: activityv1.NewMessageInboxServiceClient(conn),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,6 +231,42 @@ func (c *GRPCClient) ListWeeklyStarSettlements(ctx context.Context, req *activit
|
|||||||
return c.weeklyStarClient.ListWeeklyStarSettlements(ctx, req)
|
return c.weeklyStarClient.ListWeeklyStarSettlements(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) ListAgencyOpeningCycles(ctx context.Context, req *activityv1.ListAgencyOpeningCyclesRequest) (*activityv1.ListAgencyOpeningCyclesResponse, error) {
|
||||||
|
return c.agencyOpeningClient.ListAgencyOpeningCycles(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) CreateAgencyOpeningCycle(ctx context.Context, req *activityv1.UpsertAgencyOpeningCycleRequest) (*activityv1.UpsertAgencyOpeningCycleResponse, error) {
|
||||||
|
return c.agencyOpeningClient.CreateAgencyOpeningCycle(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) GetAgencyOpeningCycle(ctx context.Context, req *activityv1.GetAgencyOpeningCycleRequest) (*activityv1.GetAgencyOpeningCycleResponse, error) {
|
||||||
|
return c.agencyOpeningClient.GetAgencyOpeningCycle(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) UpdateAgencyOpeningCycle(ctx context.Context, req *activityv1.UpsertAgencyOpeningCycleRequest) (*activityv1.UpsertAgencyOpeningCycleResponse, error) {
|
||||||
|
return c.agencyOpeningClient.UpdateAgencyOpeningCycle(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) SetAgencyOpeningCycleStatus(ctx context.Context, req *activityv1.SetAgencyOpeningCycleStatusRequest) (*activityv1.SetAgencyOpeningCycleStatusResponse, error) {
|
||||||
|
return c.agencyOpeningClient.SetAgencyOpeningCycleStatus(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) ListAgencyOpeningApplications(ctx context.Context, req *activityv1.ListAgencyOpeningApplicationsRequest) (*activityv1.ListAgencyOpeningApplicationsResponse, error) {
|
||||||
|
return c.agencyOpeningClient.ListAgencyOpeningApplications(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) GetCPWeeklyRankConfig(ctx context.Context, req *activityv1.GetCPWeeklyRankConfigRequest) (*activityv1.GetCPWeeklyRankConfigResponse, error) {
|
||||||
|
return c.cpWeeklyRankClient.GetCPWeeklyRankConfig(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) UpdateCPWeeklyRankConfig(ctx context.Context, req *activityv1.UpdateCPWeeklyRankConfigRequest) (*activityv1.UpdateCPWeeklyRankConfigResponse, error) {
|
||||||
|
return c.cpWeeklyRankClient.UpdateCPWeeklyRankConfig(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) ListCPWeeklyRankSettlements(ctx context.Context, req *activityv1.ListCPWeeklyRankSettlementsRequest) (*activityv1.ListCPWeeklyRankSettlementsResponse, error) {
|
||||||
|
return c.cpWeeklyRankClient.ListCPWeeklyRankSettlements(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *GRPCClient) GetLuckyGiftConfig(ctx context.Context, req *activityv1.GetLuckyGiftConfigRequest) (*activityv1.GetLuckyGiftConfigResponse, error) {
|
func (c *GRPCClient) GetLuckyGiftConfig(ctx context.Context, req *activityv1.GetLuckyGiftConfigRequest) (*activityv1.GetLuckyGiftConfigResponse, error) {
|
||||||
return c.luckyGiftClient.GetLuckyGiftConfig(ctx, req)
|
return c.luckyGiftClient.GetLuckyGiftConfig(ctx, req)
|
||||||
}
|
}
|
||||||
@ -253,3 +306,11 @@ func (c *GRPCClient) UpsertLevelRule(ctx context.Context, req *activityv1.Upsert
|
|||||||
func (c *GRPCClient) UpsertLevelTier(ctx context.Context, req *activityv1.UpsertLevelTierRequest) (*activityv1.UpsertLevelTierResponse, error) {
|
func (c *GRPCClient) UpsertLevelTier(ctx context.Context, req *activityv1.UpsertLevelTierRequest) (*activityv1.UpsertLevelTierResponse, error) {
|
||||||
return c.growthClient.UpsertLevelTier(ctx, req)
|
return c.growthClient.UpsertLevelTier(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) CreateInboxMessage(ctx context.Context, req *activityv1.CreateInboxMessageRequest) (*activityv1.CreateInboxMessageResponse, error) {
|
||||||
|
return c.messageClient.CreateInboxMessage(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) CreateFanoutJob(ctx context.Context, req *activityv1.CreateFanoutJobRequest) (*activityv1.CreateFanoutJobResponse, error) {
|
||||||
|
return c.messageClient.CreateFanoutJob(ctx, req)
|
||||||
|
}
|
||||||
|
|||||||
299
server/admin/internal/modules/agencyopening/handler.go
Normal file
299
server/admin/internal/modules/agencyopening/handler.go
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
package agencyopening
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp-admin-server/internal/appctx"
|
||||||
|
"hyapp-admin-server/internal/integration/activityclient"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
activity activityclient.Client
|
||||||
|
audit shared.OperationLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(activity activityclient.Client, audit shared.OperationLogger) *Handler {
|
||||||
|
return &Handler{activity: activity, audit: audit}
|
||||||
|
}
|
||||||
|
|
||||||
|
type cycleRequest struct {
|
||||||
|
CycleID string `json:"cycle_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
StartMS int64 `json:"start_ms"`
|
||||||
|
EndMS int64 `json:"end_ms"`
|
||||||
|
MinHostCount int32 `json:"min_host_count"`
|
||||||
|
MaxAgencyAgeDays int32 `json:"max_agency_age_days"`
|
||||||
|
Rewards []rewardDTO `json:"rewards"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type statusRequest struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cycleDTO struct {
|
||||||
|
AppCode string `json:"app_code,omitempty"`
|
||||||
|
CycleID string `json:"cycle_id"`
|
||||||
|
ActivityCode string `json:"activity_code"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
StartMS int64 `json:"start_ms"`
|
||||||
|
EndMS int64 `json:"end_ms"`
|
||||||
|
MinHostCount int32 `json:"min_host_count"`
|
||||||
|
MaxAgencyAgeDays int32 `json:"max_agency_age_days"`
|
||||||
|
Rewards []rewardDTO `json:"rewards"`
|
||||||
|
CreatedByAdminID int64 `json:"created_by_admin_id"`
|
||||||
|
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
|
||||||
|
SettledAtMS int64 `json:"settled_at_ms"`
|
||||||
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type rewardDTO struct {
|
||||||
|
RankNo int32 `json:"rank_no"`
|
||||||
|
TierNo int32 `json:"tier_no,omitempty"`
|
||||||
|
ThresholdCoinSpent int64 `json:"threshold_coin_spent"`
|
||||||
|
RewardCoinAmount int64 `json:"reward_coin_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type applicationDTO struct {
|
||||||
|
ApplicationID string `json:"application_id"`
|
||||||
|
CycleID string `json:"cycle_id"`
|
||||||
|
AgencyID int64 `json:"agency_id,string"`
|
||||||
|
AgencyOwnerUserID int64 `json:"agency_owner_user_id,string"`
|
||||||
|
AgencyName string `json:"agency_name"`
|
||||||
|
HostCount int32 `json:"host_count"`
|
||||||
|
AgencyCreatedAtMS int64 `json:"agency_created_at_ms"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
ScoreCoinAmount int64 `json:"score_coin_amount"`
|
||||||
|
RewardRankNo int32 `json:"reward_rank_no"`
|
||||||
|
RewardThresholdCoin int64 `json:"reward_threshold_coin_spent"`
|
||||||
|
RewardCoinAmount int64 `json:"reward_coin_amount"`
|
||||||
|
WalletCommandID string `json:"wallet_command_id"`
|
||||||
|
WalletTransactionID string `json:"wallet_transaction_id"`
|
||||||
|
FailureReason string `json:"failure_reason"`
|
||||||
|
AppliedAtMS int64 `json:"applied_at_ms"`
|
||||||
|
GrantedAtMS int64 `json:"granted_at_ms"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListCycles(c *gin.Context) {
|
||||||
|
options := shared.ListOptions(c)
|
||||||
|
resp, err := h.activity.ListAgencyOpeningCycles(c.Request.Context(), &activityv1.ListAgencyOpeningCyclesRequest{
|
||||||
|
Meta: h.meta(c),
|
||||||
|
Status: strings.TrimSpace(options.Status),
|
||||||
|
StartMs: parseInt64Query(c, "start_ms"),
|
||||||
|
EndMs: parseInt64Query(c, "end_ms"),
|
||||||
|
Page: int32(options.Page),
|
||||||
|
PageSize: int32(options.PageSize),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取代理开业周期失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]cycleDTO, 0, len(resp.GetCycles()))
|
||||||
|
for _, cycle := range resp.GetCycles() {
|
||||||
|
items = append(items, cycleFromProto(cycle))
|
||||||
|
}
|
||||||
|
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CreateCycle(c *gin.Context) {
|
||||||
|
var req cycleRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "代理开业周期参数不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.activity.CreateAgencyOpeningCycle(c.Request.Context(), &activityv1.UpsertAgencyOpeningCycleRequest{
|
||||||
|
Meta: h.meta(c),
|
||||||
|
Cycle: req.toProto(),
|
||||||
|
OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item := cycleFromProto(resp.GetCycle())
|
||||||
|
shared.OperationLogWithResourceID(c, h.audit, "create-agency-opening-cycle", "agency_opening_cycles", item.CycleID, "success", "")
|
||||||
|
response.OK(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetCycle(c *gin.Context) {
|
||||||
|
cycleID := strings.TrimSpace(c.Param("cycle_id"))
|
||||||
|
resp, err := h.activity.GetAgencyOpeningCycle(c.Request.Context(), &activityv1.GetAgencyOpeningCycleRequest{Meta: h.meta(c), CycleId: cycleID})
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取代理开业周期详情失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, cycleFromProto(resp.GetCycle()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UpdateCycle(c *gin.Context) {
|
||||||
|
var req cycleRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "代理开业周期参数不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.CycleID = strings.TrimSpace(c.Param("cycle_id"))
|
||||||
|
resp, err := h.activity.UpdateAgencyOpeningCycle(c.Request.Context(), &activityv1.UpsertAgencyOpeningCycleRequest{
|
||||||
|
Meta: h.meta(c),
|
||||||
|
Cycle: req.toProto(),
|
||||||
|
OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item := cycleFromProto(resp.GetCycle())
|
||||||
|
shared.OperationLogWithResourceID(c, h.audit, "update-agency-opening-cycle", "agency_opening_cycles", item.CycleID, "success", "")
|
||||||
|
response.OK(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) SetCycleStatus(c *gin.Context) {
|
||||||
|
var req statusRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "代理开业周期状态参数不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cycleID := strings.TrimSpace(c.Param("cycle_id"))
|
||||||
|
resp, err := h.activity.SetAgencyOpeningCycleStatus(c.Request.Context(), &activityv1.SetAgencyOpeningCycleStatusRequest{
|
||||||
|
Meta: h.meta(c),
|
||||||
|
CycleId: cycleID,
|
||||||
|
Status: strings.TrimSpace(req.Status),
|
||||||
|
OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item := cycleFromProto(resp.GetCycle())
|
||||||
|
shared.OperationLogWithResourceID(c, h.audit, "set-agency-opening-cycle-status", "agency_opening_cycles", item.CycleID, "success", "")
|
||||||
|
response.OK(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListApplications(c *gin.Context) {
|
||||||
|
options := shared.ListOptions(c)
|
||||||
|
resp, err := h.activity.ListAgencyOpeningApplications(c.Request.Context(), &activityv1.ListAgencyOpeningApplicationsRequest{
|
||||||
|
Meta: h.meta(c),
|
||||||
|
CycleId: strings.TrimSpace(c.Query("cycle_id")),
|
||||||
|
Status: strings.TrimSpace(options.Status),
|
||||||
|
AgencyId: parseInt64Query(c, "agency_id"),
|
||||||
|
AgencyOwnerUserId: parseInt64Query(c, "agency_owner_user_id"),
|
||||||
|
Page: int32(options.Page),
|
||||||
|
PageSize: int32(options.PageSize),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取代理开业申请记录失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]applicationDTO, 0, len(resp.GetApplications()))
|
||||||
|
for _, application := range resp.GetApplications() {
|
||||||
|
items = append(items, applicationFromProto(application))
|
||||||
|
}
|
||||||
|
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||||||
|
}
|
||||||
|
|
||||||
|
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().UnixMilli(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r cycleRequest) toProto() *activityv1.AgencyOpeningCycle {
|
||||||
|
rewards := make([]*activityv1.AgencyOpeningReward, 0, len(r.Rewards))
|
||||||
|
for _, reward := range r.Rewards {
|
||||||
|
rankNo := reward.RankNo
|
||||||
|
if rankNo <= 0 {
|
||||||
|
rankNo = reward.TierNo
|
||||||
|
}
|
||||||
|
rewards = append(rewards, &activityv1.AgencyOpeningReward{
|
||||||
|
RankNo: rankNo,
|
||||||
|
ThresholdCoinSpent: reward.ThresholdCoinSpent,
|
||||||
|
RewardCoinAmount: reward.RewardCoinAmount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &activityv1.AgencyOpeningCycle{
|
||||||
|
CycleId: strings.TrimSpace(r.CycleID),
|
||||||
|
Title: strings.TrimSpace(r.Title),
|
||||||
|
Status: strings.TrimSpace(r.Status),
|
||||||
|
StartMs: r.StartMS,
|
||||||
|
EndMs: r.EndMS,
|
||||||
|
MinHostCount: r.MinHostCount,
|
||||||
|
MaxAgencyAgeDays: r.MaxAgencyAgeDays,
|
||||||
|
Rewards: rewards,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cycleFromProto(cycle *activityv1.AgencyOpeningCycle) cycleDTO {
|
||||||
|
if cycle == nil {
|
||||||
|
return cycleDTO{Rewards: []rewardDTO{}}
|
||||||
|
}
|
||||||
|
rewards := make([]rewardDTO, 0, len(cycle.GetRewards()))
|
||||||
|
for _, reward := range cycle.GetRewards() {
|
||||||
|
rewards = append(rewards, rewardDTO{
|
||||||
|
RankNo: reward.GetRankNo(),
|
||||||
|
TierNo: reward.GetRankNo(),
|
||||||
|
ThresholdCoinSpent: reward.GetThresholdCoinSpent(),
|
||||||
|
RewardCoinAmount: reward.GetRewardCoinAmount(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cycleDTO{
|
||||||
|
AppCode: cycle.GetAppCode(),
|
||||||
|
CycleID: cycle.GetCycleId(),
|
||||||
|
ActivityCode: cycle.GetActivityCode(),
|
||||||
|
Title: cycle.GetTitle(),
|
||||||
|
Status: cycle.GetStatus(),
|
||||||
|
StartMS: cycle.GetStartMs(),
|
||||||
|
EndMS: cycle.GetEndMs(),
|
||||||
|
MinHostCount: cycle.GetMinHostCount(),
|
||||||
|
MaxAgencyAgeDays: cycle.GetMaxAgencyAgeDays(),
|
||||||
|
Rewards: rewards,
|
||||||
|
CreatedByAdminID: cycle.GetCreatedByAdminId(),
|
||||||
|
UpdatedByAdminID: cycle.GetUpdatedByAdminId(),
|
||||||
|
SettledAtMS: cycle.GetSettledAtMs(),
|
||||||
|
CreatedAtMS: cycle.GetCreatedAtMs(),
|
||||||
|
UpdatedAtMS: cycle.GetUpdatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationFromProto(item *activityv1.AgencyOpeningApplication) applicationDTO {
|
||||||
|
if item == nil {
|
||||||
|
return applicationDTO{}
|
||||||
|
}
|
||||||
|
return applicationDTO{
|
||||||
|
ApplicationID: item.GetApplicationId(),
|
||||||
|
CycleID: item.GetCycleId(),
|
||||||
|
AgencyID: item.GetAgencyId(),
|
||||||
|
AgencyOwnerUserID: item.GetAgencyOwnerUserId(),
|
||||||
|
AgencyName: item.GetAgencyName(),
|
||||||
|
HostCount: item.GetHostCount(),
|
||||||
|
AgencyCreatedAtMS: item.GetAgencyCreatedAtMs(),
|
||||||
|
Status: item.GetStatus(),
|
||||||
|
ScoreCoinAmount: item.GetScoreCoinAmount(),
|
||||||
|
RewardRankNo: item.GetRewardRankNo(),
|
||||||
|
RewardThresholdCoin: item.GetRewardThresholdCoinSpent(),
|
||||||
|
RewardCoinAmount: item.GetRewardCoinAmount(),
|
||||||
|
WalletCommandID: item.GetWalletCommandId(),
|
||||||
|
WalletTransactionID: item.GetWalletTransactionId(),
|
||||||
|
FailureReason: item.GetFailureReason(),
|
||||||
|
AppliedAtMS: item.GetAppliedAtMs(),
|
||||||
|
GrantedAtMS: item.GetGrantedAtMs(),
|
||||||
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInt64Query(c *gin.Context, key string) int64 {
|
||||||
|
value, _ := strconv.ParseInt(strings.TrimSpace(c.Query(key)), 10, 64)
|
||||||
|
return value
|
||||||
|
}
|
||||||
19
server/admin/internal/modules/agencyopening/routes.go
Normal file
19
server/admin/internal/modules/agencyopening/routes.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package agencyopening
|
||||||
|
|
||||||
|
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/activity/agency-opening/cycles", middleware.RequirePermission("agency-opening:view"), h.ListCycles)
|
||||||
|
protected.POST("/admin/activity/agency-opening/cycles", middleware.RequirePermission("agency-opening:create"), h.CreateCycle)
|
||||||
|
protected.GET("/admin/activity/agency-opening/cycles/:cycle_id", middleware.RequirePermission("agency-opening:view"), h.GetCycle)
|
||||||
|
protected.PUT("/admin/activity/agency-opening/cycles/:cycle_id", middleware.RequirePermission("agency-opening:update"), h.UpdateCycle)
|
||||||
|
protected.PATCH("/admin/activity/agency-opening/cycles/:cycle_id/status", middleware.RequirePermission("agency-opening:update"), h.SetCycleStatus)
|
||||||
|
protected.GET("/admin/activity/agency-opening/applications", middleware.RequirePermission("agency-opening:view"), h.ListApplications)
|
||||||
|
}
|
||||||
@ -5,6 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"hyapp-admin-server/internal/appctx"
|
"hyapp-admin-server/internal/appctx"
|
||||||
|
"hyapp-admin-server/internal/integration/activityclient"
|
||||||
|
"hyapp-admin-server/internal/middleware"
|
||||||
"hyapp-admin-server/internal/modules/shared"
|
"hyapp-admin-server/internal/modules/shared"
|
||||||
"hyapp-admin-server/internal/response"
|
"hyapp-admin-server/internal/response"
|
||||||
|
|
||||||
@ -17,7 +19,11 @@ type Handler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(userDB DB, audit shared.OperationLogger) *Handler {
|
func New(userDB DB, audit shared.OperationLogger) *Handler {
|
||||||
return &Handler{service: NewService(userDB), audit: audit}
|
return NewWithActivity(userDB, nil, audit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWithActivity(userDB DB, activity activityclient.Client, audit shared.OperationLogger) *Handler {
|
||||||
|
return &Handler{service: NewService(userDB, activity), audit: audit}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) GetConfig(c *gin.Context) {
|
func (h *Handler) GetConfig(c *gin.Context) {
|
||||||
@ -35,6 +41,9 @@ func (h *Handler) UpdateConfig(c *gin.Context) {
|
|||||||
response.BadRequest(c, "CP配置参数不正确")
|
response.BadRequest(c, "CP配置参数不正确")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if req.WeeklyRank != nil {
|
||||||
|
req.WeeklyRank.UpdatedByAdminID = int64(middleware.CurrentUserID(c))
|
||||||
|
}
|
||||||
config, err := h.service.UpdateConfig(c.Request.Context(), appctx.FromContext(c.Request.Context()), req)
|
config, err := h.service.UpdateConfig(c.Request.Context(), appctx.FromContext(c.Request.Context()), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrInvalidArgument) {
|
if errors.Is(err, ErrInvalidArgument) {
|
||||||
|
|||||||
@ -8,6 +8,9 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"hyapp-admin-server/internal/integration/activityclient"
|
||||||
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -38,12 +41,14 @@ type DB interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
db DB
|
db DB
|
||||||
|
activity activityclient.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type configDTO struct {
|
type configDTO struct {
|
||||||
AppCode string `json:"appCode"`
|
AppCode string `json:"appCode"`
|
||||||
Relations []relationDTO `json:"relations"`
|
Relations []relationDTO `json:"relations"`
|
||||||
|
WeeklyRank weeklyRankDTO `json:"weeklyRank"`
|
||||||
ServerTimeMS int64 `json:"serverTimeMs"`
|
ServerTimeMS int64 `json:"serverTimeMs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,12 +72,28 @@ type levelDTO struct {
|
|||||||
UpdatedAtMS int64 `json:"updatedAtMs"`
|
UpdatedAtMS int64 `json:"updatedAtMs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type updateRequest struct {
|
type weeklyRankDTO struct {
|
||||||
Relations []relationDTO `json:"relations"`
|
Enabled bool `json:"enabled"`
|
||||||
|
RelationType string `json:"relationType"`
|
||||||
|
TopCount int32 `json:"topCount"`
|
||||||
|
Rewards []weeklyRankRewardDTO `json:"rewards"`
|
||||||
|
UpdatedByAdminID int64 `json:"updatedByAdminId"`
|
||||||
|
CreatedAtMS int64 `json:"createdAtMs"`
|
||||||
|
UpdatedAtMS int64 `json:"updatedAtMs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(db DB) *Service {
|
type weeklyRankRewardDTO struct {
|
||||||
return &Service{db: db}
|
RankNo int32 `json:"rankNo"`
|
||||||
|
ResourceGroupID int64 `json:"resourceGroupId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateRequest struct {
|
||||||
|
Relations []relationDTO `json:"relations"`
|
||||||
|
WeeklyRank *weeklyRankDTO `json:"weeklyRank"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(db DB, activity activityclient.Client) *Service {
|
||||||
|
return &Service{db: db, activity: activity}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfig 读取当前 App 的关系容量和等级阈值;缺配置时先落默认值,保证后台首次打开就是可保存状态。
|
// GetConfig 读取当前 App 的关系容量和等级阈值;缺配置时先落默认值,保证后台首次打开就是可保存状态。
|
||||||
@ -88,7 +109,11 @@ func (s *Service) GetConfig(ctx context.Context, appCode string) (configDTO, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return configDTO{}, err
|
return configDTO{}, err
|
||||||
}
|
}
|
||||||
return configDTO{AppCode: appCode, Relations: relations, ServerTimeMS: nowMs}, nil
|
weeklyRank, err := s.getWeeklyRankConfig(ctx, appCode)
|
||||||
|
if err != nil {
|
||||||
|
return configDTO{}, err
|
||||||
|
}
|
||||||
|
return configDTO{AppCode: appCode, Relations: relations, WeeklyRank: weeklyRank, ServerTimeMS: nowMs}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateConfig 覆盖低频配置;写入时按 relation_type + level_no upsert,运行中消费逻辑每次读取最新阈值。
|
// UpdateConfig 覆盖低频配置;写入时按 relation_type + level_no upsert,运行中消费逻辑每次读取最新阈值。
|
||||||
@ -146,9 +171,80 @@ func (s *Service) UpdateConfig(ctx context.Context, appCode string, req updateRe
|
|||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
return configDTO{}, err
|
return configDTO{}, err
|
||||||
}
|
}
|
||||||
|
if req.WeeklyRank != nil {
|
||||||
|
// CP 关系配置归 user DB,周榜启停和发奖事实归 activity-service;这里在 user 配置提交后再写 activity,
|
||||||
|
// 失败时返回错误让后台明确重试,避免误以为奖励配置已经生效。
|
||||||
|
if err := s.updateWeeklyRankConfig(ctx, appCode, *req.WeeklyRank); err != nil {
|
||||||
|
return configDTO{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return s.GetConfig(ctx, appCode)
|
return s.GetConfig(ctx, appCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) getWeeklyRankConfig(ctx context.Context, appCode string) (weeklyRankDTO, error) {
|
||||||
|
if s.activity == nil {
|
||||||
|
return weeklyRankDTO{Enabled: true, RelationType: relationTypeCP, TopCount: 3}, nil
|
||||||
|
}
|
||||||
|
resp, err := s.activity.GetCPWeeklyRankConfig(ctx, &activityv1.GetCPWeeklyRankConfigRequest{Meta: s.activityMeta(appCode)})
|
||||||
|
if err != nil {
|
||||||
|
return weeklyRankDTO{}, err
|
||||||
|
}
|
||||||
|
return weeklyRankFromProto(resp.GetConfig()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) updateWeeklyRankConfig(ctx context.Context, appCode string, req weeklyRankDTO) error {
|
||||||
|
if s.activity == nil {
|
||||||
|
return errors.New("activity client is not configured")
|
||||||
|
}
|
||||||
|
_, err := s.activity.UpdateCPWeeklyRankConfig(ctx, &activityv1.UpdateCPWeeklyRankConfigRequest{
|
||||||
|
Meta: s.activityMeta(appCode),
|
||||||
|
Config: weeklyRankToProto(req),
|
||||||
|
OperatorAdminId: req.UpdatedByAdminID,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) activityMeta(appCode string) *activityv1.RequestMeta {
|
||||||
|
return &activityv1.RequestMeta{
|
||||||
|
RequestId: "admin-cp-config",
|
||||||
|
Caller: "admin-server",
|
||||||
|
AppCode: appCode,
|
||||||
|
SentAtMs: time.Now().UTC().UnixMilli(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func weeklyRankFromProto(item *activityv1.CPWeeklyRankConfig) weeklyRankDTO {
|
||||||
|
if item == nil {
|
||||||
|
return weeklyRankDTO{Enabled: true, RelationType: relationTypeCP, TopCount: 3}
|
||||||
|
}
|
||||||
|
rewards := make([]weeklyRankRewardDTO, 0, len(item.GetRewards()))
|
||||||
|
for _, reward := range item.GetRewards() {
|
||||||
|
rewards = append(rewards, weeklyRankRewardDTO{RankNo: reward.GetRankNo(), ResourceGroupID: reward.GetResourceGroupId()})
|
||||||
|
}
|
||||||
|
return weeklyRankDTO{
|
||||||
|
Enabled: item.GetEnabled(),
|
||||||
|
RelationType: item.GetRelationType(),
|
||||||
|
TopCount: item.GetTopCount(),
|
||||||
|
Rewards: rewards,
|
||||||
|
UpdatedByAdminID: item.GetUpdatedByAdminId(),
|
||||||
|
CreatedAtMS: item.GetCreatedAtMs(),
|
||||||
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func weeklyRankToProto(item weeklyRankDTO) *activityv1.CPWeeklyRankConfig {
|
||||||
|
rewards := make([]*activityv1.CPWeeklyRankReward, 0, len(item.Rewards))
|
||||||
|
for _, reward := range item.Rewards {
|
||||||
|
rewards = append(rewards, &activityv1.CPWeeklyRankReward{RankNo: reward.RankNo, ResourceGroupId: reward.ResourceGroupID})
|
||||||
|
}
|
||||||
|
return &activityv1.CPWeeklyRankConfig{
|
||||||
|
Enabled: item.Enabled,
|
||||||
|
RelationType: relationTypeCP,
|
||||||
|
TopCount: item.TopCount,
|
||||||
|
Rewards: rewards,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) listRelations(ctx context.Context, appCode string) ([]relationDTO, error) {
|
func (s *Service) listRelations(ctx context.Context, appCode string) ([]relationDTO, error) {
|
||||||
rows, err := s.db.QueryContext(ctx, `
|
rows, err := s.db.QueryContext(ctx, `
|
||||||
SELECT relation_type, max_count_per_user, application_expire_hours, breakup_cost_coins, status, updated_at_ms
|
SELECT relation_type, max_count_per_user, application_expire_hours, breakup_cost_coins, status, updated_at_ms
|
||||||
|
|||||||
301
server/admin/internal/modules/fullservernotice/handler.go
Normal file
301
server/admin/internal/modules/fullservernotice/handler.go
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
package fullservernotice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp-admin-server/internal/appctx"
|
||||||
|
"hyapp-admin-server/internal/integration/activityclient"
|
||||||
|
"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 (
|
||||||
|
messageTypeSystem = "system"
|
||||||
|
messageTypeActivity = "activity"
|
||||||
|
|
||||||
|
targetScopeSingleUser = "single_user"
|
||||||
|
targetScopeUserIDs = "user_ids"
|
||||||
|
targetScopeRegion = "region"
|
||||||
|
targetScopeCountry = "country"
|
||||||
|
targetScopeAllActiveUsers = "all_active_users"
|
||||||
|
|
||||||
|
defaultProducerEventType = "admin_full_server_notice"
|
||||||
|
defaultBatchSize = 500
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
activity activityclient.Client
|
||||||
|
requestTimeout time.Duration
|
||||||
|
audit shared.OperationLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(activity activityclient.Client, audit shared.OperationLogger) *Handler {
|
||||||
|
return &Handler{activity: activity, requestTimeout: 3 * time.Second, audit: audit}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fanoutRequest struct {
|
||||||
|
CommandID string `json:"command_id"`
|
||||||
|
MessageType string `json:"message_type"`
|
||||||
|
TargetScope string `json:"target_scope"`
|
||||||
|
TargetUserID int64 `json:"target_user_id"`
|
||||||
|
UserIDs []int64 `json:"user_ids"`
|
||||||
|
RegionID int64 `json:"region_id"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
ProducerEventType string `json:"producer_event_type"`
|
||||||
|
AggregateType string `json:"aggregate_type"`
|
||||||
|
AggregateID string `json:"aggregate_id"`
|
||||||
|
TemplateID string `json:"template_id"`
|
||||||
|
TemplateVersion string `json:"template_version"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
IconURL string `json:"icon_url"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
ActionType string `json:"action_type"`
|
||||||
|
ActionParam string `json:"action_param"`
|
||||||
|
Priority int32 `json:"priority"`
|
||||||
|
SentAtMS int64 `json:"sent_at_ms"`
|
||||||
|
ExpireAtMS int64 `json:"expire_at_ms"`
|
||||||
|
MetadataJSON string `json:"metadata_json"`
|
||||||
|
BatchSize int32 `json:"batch_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type fanoutDTO struct {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Created bool `json:"created"`
|
||||||
|
CommandID string `json:"command_id"`
|
||||||
|
MessageType string `json:"message_type"`
|
||||||
|
TargetScope string `json:"target_scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CreateFanout(c *gin.Context) {
|
||||||
|
var req fanoutRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "全服通知参数不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
normalized, err := normalizeFanoutRequest(req, int64(middleware.CurrentUserID(c)))
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetFilterJSON, err := buildTargetFilterJSON(normalized)
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
templateSnapshotJSON, err := buildTemplateSnapshotJSON(normalized)
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := h.activityContext(c)
|
||||||
|
defer cancel()
|
||||||
|
// 全服通知只向 activity-service 创建 fanout 命令;真正逐用户写入由 message fanout worker 执行。
|
||||||
|
// command_id 作为后台操作幂等键,重复提交同一 payload 会返回同一个 job,不会重复刷用户收件箱。
|
||||||
|
resp, err := h.activity.CreateFanoutJob(ctx, &activityv1.CreateFanoutJobRequest{
|
||||||
|
Meta: h.meta(c),
|
||||||
|
CommandId: normalized.CommandID,
|
||||||
|
MessageType: normalized.MessageType,
|
||||||
|
TargetScope: normalized.TargetScope,
|
||||||
|
TargetFilterJson: targetFilterJSON,
|
||||||
|
TemplateSnapshotJson: templateSnapshotJSON,
|
||||||
|
BatchSize: normalized.BatchSize,
|
||||||
|
CreatedBy: "admin:" + strconv.FormatInt(int64(middleware.CurrentUserID(c)), 10),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item := fanoutDTO{
|
||||||
|
JobID: resp.GetJobId(),
|
||||||
|
Status: resp.GetStatus(),
|
||||||
|
Created: resp.GetCreated(),
|
||||||
|
CommandID: normalized.CommandID,
|
||||||
|
MessageType: normalized.MessageType,
|
||||||
|
TargetScope: normalized.TargetScope,
|
||||||
|
}
|
||||||
|
shared.OperationLogWithResourceID(c, h.audit, "create-full-server-notice-fanout", "message_fanout_jobs", item.JobID, "success", normalized.Title)
|
||||||
|
response.Created(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) activityContext(c *gin.Context) (context.Context, context.CancelFunc) {
|
||||||
|
timeout := h.requestTimeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 3 * time.Second
|
||||||
|
}
|
||||||
|
return context.WithTimeout(c.Request.Context(), timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeFanoutRequest(req fanoutRequest, adminID int64) (fanoutRequest, error) {
|
||||||
|
req.MessageType = strings.ToLower(strings.TrimSpace(req.MessageType))
|
||||||
|
if req.MessageType == "" {
|
||||||
|
req.MessageType = messageTypeSystem
|
||||||
|
}
|
||||||
|
if req.MessageType != messageTypeSystem && req.MessageType != messageTypeActivity {
|
||||||
|
return fanoutRequest{}, fmt.Errorf("message_type 只支持 system 或 activity")
|
||||||
|
}
|
||||||
|
req.TargetScope = strings.TrimSpace(req.TargetScope)
|
||||||
|
if req.TargetScope == "" {
|
||||||
|
req.TargetScope = targetScopeAllActiveUsers
|
||||||
|
}
|
||||||
|
if !allowedTargetScope(req.TargetScope) {
|
||||||
|
return fanoutRequest{}, fmt.Errorf("target_scope 不正确")
|
||||||
|
}
|
||||||
|
req.Title = strings.TrimSpace(req.Title)
|
||||||
|
req.Summary = strings.TrimSpace(req.Summary)
|
||||||
|
req.Body = strings.TrimSpace(req.Body)
|
||||||
|
req.IconURL = strings.TrimSpace(req.IconURL)
|
||||||
|
req.ImageURL = strings.TrimSpace(req.ImageURL)
|
||||||
|
req.ActionType = strings.TrimSpace(req.ActionType)
|
||||||
|
req.ActionParam = strings.TrimSpace(req.ActionParam)
|
||||||
|
req.ProducerEventType = firstText(req.ProducerEventType, defaultProducerEventType)
|
||||||
|
req.AggregateType = firstText(req.AggregateType, "admin_notice")
|
||||||
|
req.AggregateID = strings.TrimSpace(req.AggregateID)
|
||||||
|
req.TemplateID = strings.TrimSpace(req.TemplateID)
|
||||||
|
req.TemplateVersion = strings.TrimSpace(req.TemplateVersion)
|
||||||
|
req.Country = strings.TrimSpace(req.Country)
|
||||||
|
req.MetadataJSON = strings.TrimSpace(req.MetadataJSON)
|
||||||
|
if req.Title == "" || req.Summary == "" {
|
||||||
|
return fanoutRequest{}, fmt.Errorf("title 和 summary 不能为空")
|
||||||
|
}
|
||||||
|
if req.ExpireAtMS > 0 && req.SentAtMS > 0 && req.ExpireAtMS <= req.SentAtMS {
|
||||||
|
return fanoutRequest{}, fmt.Errorf("expire_at_ms 必须晚于 sent_at_ms")
|
||||||
|
}
|
||||||
|
if req.MetadataJSON != "" && !json.Valid([]byte(req.MetadataJSON)) {
|
||||||
|
return fanoutRequest{}, fmt.Errorf("metadata_json 不是合法 JSON")
|
||||||
|
}
|
||||||
|
if req.BatchSize <= 0 {
|
||||||
|
req.BatchSize = defaultBatchSize
|
||||||
|
}
|
||||||
|
if req.CommandID == "" {
|
||||||
|
req.CommandID = fmt.Sprintf("admin_notice_%d_%d", adminID, time.Now().UTC().UnixMilli())
|
||||||
|
}
|
||||||
|
req.CommandID = strings.TrimSpace(req.CommandID)
|
||||||
|
if req.AggregateID == "" {
|
||||||
|
req.AggregateID = req.CommandID
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTargetFilterJSON(req fanoutRequest) (string, error) {
|
||||||
|
var payload map[string]any
|
||||||
|
switch req.TargetScope {
|
||||||
|
case targetScopeAllActiveUsers:
|
||||||
|
payload = map[string]any{}
|
||||||
|
case targetScopeSingleUser:
|
||||||
|
if req.TargetUserID <= 0 {
|
||||||
|
return "", fmt.Errorf("target_user_id 不能为空")
|
||||||
|
}
|
||||||
|
payload = map[string]any{"target_user_id": req.TargetUserID}
|
||||||
|
case targetScopeUserIDs:
|
||||||
|
userIDs := positiveUniqueInt64s(req.UserIDs)
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return "", fmt.Errorf("user_ids 不能为空")
|
||||||
|
}
|
||||||
|
payload = map[string]any{"user_ids": userIDs}
|
||||||
|
case targetScopeRegion:
|
||||||
|
if req.RegionID <= 0 {
|
||||||
|
return "", fmt.Errorf("region_id 不能为空")
|
||||||
|
}
|
||||||
|
payload = map[string]any{"region_id": req.RegionID}
|
||||||
|
case targetScopeCountry:
|
||||||
|
if req.Country == "" {
|
||||||
|
return "", fmt.Errorf("country 不能为空")
|
||||||
|
}
|
||||||
|
payload = map[string]any{"country": req.Country}
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("target_scope 不正确")
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(encoded), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTemplateSnapshotJSON(req fanoutRequest) (string, error) {
|
||||||
|
payload := map[string]any{
|
||||||
|
"producer_event_type": req.ProducerEventType,
|
||||||
|
"aggregate_type": req.AggregateType,
|
||||||
|
"aggregate_id": req.AggregateID,
|
||||||
|
"template_id": req.TemplateID,
|
||||||
|
"template_version": req.TemplateVersion,
|
||||||
|
"title": req.Title,
|
||||||
|
"summary": req.Summary,
|
||||||
|
"body": req.Body,
|
||||||
|
"icon_url": req.IconURL,
|
||||||
|
"image_url": req.ImageURL,
|
||||||
|
"action_type": req.ActionType,
|
||||||
|
"action_param": req.ActionParam,
|
||||||
|
"priority": req.Priority,
|
||||||
|
"sent_at_ms": req.SentAtMS,
|
||||||
|
"expire_at_ms": req.ExpireAtMS,
|
||||||
|
}
|
||||||
|
if req.MetadataJSON != "" {
|
||||||
|
var parsed any
|
||||||
|
if err := json.Unmarshal([]byte(req.MetadataJSON), &parsed); err != nil {
|
||||||
|
return "", fmt.Errorf("metadata_json 不是合法 JSON")
|
||||||
|
}
|
||||||
|
payload["metadata_json"] = parsed
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(encoded), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func allowedTargetScope(scope string) bool {
|
||||||
|
switch scope {
|
||||||
|
case targetScopeSingleUser, targetScopeUserIDs, targetScopeRegion, targetScopeCountry, targetScopeAllActiveUsers:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func positiveUniqueInt64s(values []int64) []int64 {
|
||||||
|
seen := make(map[int64]struct{}, len(values))
|
||||||
|
out := make([]int64, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
if value <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[value]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
out = append(out, value)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstText(values ...string) string {
|
||||||
|
for _, value := range values {
|
||||||
|
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package fullservernotice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildFanoutJSONForUserIDScope(t *testing.T) {
|
||||||
|
req, err := normalizeFanoutRequest(fanoutRequest{
|
||||||
|
MessageType: "activity",
|
||||||
|
TargetScope: "user_ids",
|
||||||
|
UserIDs: []int64{42, 43, 42, 0},
|
||||||
|
Title: "活动上线",
|
||||||
|
Summary: "新活动已上线。",
|
||||||
|
ActionType: "activity_detail",
|
||||||
|
ActionParam: "activity_id=act_01",
|
||||||
|
MetadataJSON: `{"source":"admin"}`,
|
||||||
|
BatchSize: 500,
|
||||||
|
}, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("normalizeFanoutRequest failed: %v", err)
|
||||||
|
}
|
||||||
|
targetFilterJSON, err := buildTargetFilterJSON(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildTargetFilterJSON failed: %v", err)
|
||||||
|
}
|
||||||
|
if targetFilterJSON != `{"user_ids":[42,43]}` {
|
||||||
|
t.Fatalf("target filter mismatch: %s", targetFilterJSON)
|
||||||
|
}
|
||||||
|
templateSnapshotJSON, err := buildTemplateSnapshotJSON(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildTemplateSnapshotJSON failed: %v", err)
|
||||||
|
}
|
||||||
|
for _, required := range []string{`"title":"活动上线"`, `"action_type":"activity_detail"`, `"metadata_json":{"source":"admin"}`} {
|
||||||
|
if !strings.Contains(templateSnapshotJSON, required) {
|
||||||
|
t.Fatalf("template snapshot %s missing %s", templateSnapshotJSON, required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeFanoutRequestRequiresTargetParameters(t *testing.T) {
|
||||||
|
req, err := normalizeFanoutRequest(fanoutRequest{
|
||||||
|
MessageType: "system",
|
||||||
|
TargetScope: "country",
|
||||||
|
Title: "维护通知",
|
||||||
|
Summary: "今晚维护。",
|
||||||
|
BatchSize: 500,
|
||||||
|
}, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("normalizeFanoutRequest failed: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := buildTargetFilterJSON(req); err == nil {
|
||||||
|
t.Fatalf("country target without country should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
14
server/admin/internal/modules/fullservernotice/routes.go
Normal file
14
server/admin/internal/modules/fullservernotice/routes.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package fullservernotice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hyapp-admin-server/internal/middleware"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||||
|
if h == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
protected.POST("/admin/operations/full-server-notices/fanout", middleware.RequirePermission("full-server-notice:send"), h.CreateFanout)
|
||||||
|
}
|
||||||
@ -101,6 +101,8 @@ var defaultPermissions = []model.Permission{
|
|||||||
{Name: "举报列表查看", Code: "report:view", Kind: "menu"},
|
{Name: "举报列表查看", Code: "report:view", Kind: "menu"},
|
||||||
{Name: "礼物钻石查看", Code: "gift-diamond:view", Kind: "menu"},
|
{Name: "礼物钻石查看", Code: "gift-diamond:view", Kind: "menu"},
|
||||||
{Name: "礼物钻石更新", Code: "gift-diamond:update", Kind: "button"},
|
{Name: "礼物钻石更新", Code: "gift-diamond:update", Kind: "button"},
|
||||||
|
{Name: "全服通知查看", Code: "full-server-notice:view", Kind: "menu"},
|
||||||
|
{Name: "全服通知发送", Code: "full-server-notice:send", Kind: "button"},
|
||||||
{Name: "支付账单查看", Code: "payment-bill:view", Kind: "menu"},
|
{Name: "支付账单查看", Code: "payment-bill:view", Kind: "menu"},
|
||||||
{Name: "三方支付查看", Code: "payment-third-party:view", Kind: "menu"},
|
{Name: "三方支付查看", Code: "payment-third-party:view", Kind: "menu"},
|
||||||
{Name: "三方支付更新", Code: "payment-third-party:update", Kind: "button"},
|
{Name: "三方支付更新", Code: "payment-third-party:update", Kind: "button"},
|
||||||
@ -140,6 +142,9 @@ var defaultPermissions = []model.Permission{
|
|||||||
{Name: "周星配置创建", Code: "weekly-star:create", Kind: "button"},
|
{Name: "周星配置创建", Code: "weekly-star:create", Kind: "button"},
|
||||||
{Name: "周星配置更新", Code: "weekly-star:update", Kind: "button"},
|
{Name: "周星配置更新", Code: "weekly-star:update", Kind: "button"},
|
||||||
{Name: "周星结算查看", Code: "weekly-star:settle", Kind: "button"},
|
{Name: "周星结算查看", Code: "weekly-star:settle", Kind: "button"},
|
||||||
|
{Name: "代理开业活动查看", Code: "agency-opening:view", Kind: "menu"},
|
||||||
|
{Name: "代理开业活动创建", Code: "agency-opening:create", Kind: "button"},
|
||||||
|
{Name: "代理开业活动更新", Code: "agency-opening:update", Kind: "button"},
|
||||||
{Name: "角色查看", Code: "role:view", Kind: "menu"},
|
{Name: "角色查看", Code: "role:view", Kind: "menu"},
|
||||||
{Name: "角色创建", Code: "role:create", Kind: "button"},
|
{Name: "角色创建", Code: "role:create", Kind: "button"},
|
||||||
{Name: "角色更新", Code: "role:update", Kind: "button"},
|
{Name: "角色更新", Code: "role:update", Kind: "button"},
|
||||||
@ -277,6 +282,7 @@ func (s *Store) seedMenus() error {
|
|||||||
{ParentID: &operationsID, Title: "幸运礼物", Code: "lucky-gift", Path: "/operations/lucky-gift", Icon: "redeem", PermissionCode: "lucky-gift:view", Sort: 71, Visible: true},
|
{ParentID: &operationsID, Title: "幸运礼物", Code: "lucky-gift", Path: "/operations/lucky-gift", Icon: "redeem", PermissionCode: "lucky-gift:view", Sort: 71, Visible: true},
|
||||||
{ParentID: &operationsID, Title: "举报列表", Code: "operation-reports", Path: "/operations/reports", Icon: "flag", PermissionCode: "report:view", Sort: 72, Visible: true},
|
{ParentID: &operationsID, Title: "举报列表", Code: "operation-reports", Path: "/operations/reports", Icon: "flag", PermissionCode: "report:view", Sort: 72, Visible: true},
|
||||||
{ParentID: &operationsID, Title: "礼物钻石", Code: "operation-gift-diamond", Path: "/operations/gift-diamonds", Icon: "diamond", PermissionCode: "gift-diamond:view", Sort: 73, Visible: true},
|
{ParentID: &operationsID, Title: "礼物钻石", Code: "operation-gift-diamond", Path: "/operations/gift-diamonds", Icon: "diamond", PermissionCode: "gift-diamond:view", Sort: 73, Visible: true},
|
||||||
|
{ParentID: &operationsID, Title: "全服通知", Code: "operation-full-server-notice", Path: "/operations/full-server-notices", Icon: "campaign", PermissionCode: "full-server-notice:view", Sort: 74, Visible: true},
|
||||||
{ParentID: &paymentID, Title: "账单列表", Code: "payment-bill-list", Path: "/payment/bills", Icon: "receipt", PermissionCode: "payment-bill:view", Sort: 68, Visible: true},
|
{ParentID: &paymentID, Title: "账单列表", Code: "payment-bill-list", Path: "/payment/bills", Icon: "receipt", PermissionCode: "payment-bill:view", Sort: 68, Visible: true},
|
||||||
{ParentID: &paymentID, Title: "三方支付", Code: "payment-third-party", Path: "/payment/third-party", Icon: "wallet", PermissionCode: "payment-third-party:view", Sort: 69, Visible: true},
|
{ParentID: &paymentID, Title: "三方支付", Code: "payment-third-party", Path: "/payment/third-party", Icon: "wallet", PermissionCode: "payment-third-party:view", Sort: 69, Visible: true},
|
||||||
{ParentID: &paymentID, Title: "支付内购商品", Code: "payment-recharge-products", Path: "/payment/recharge-products", Icon: "wallet", PermissionCode: "payment-product:view", Sort: 70, Visible: true},
|
{ParentID: &paymentID, Title: "支付内购商品", Code: "payment-recharge-products", Path: "/payment/recharge-products", Icon: "wallet", PermissionCode: "payment-product:view", Sort: 70, Visible: true},
|
||||||
@ -290,6 +296,7 @@ func (s *Store) seedMenus() error {
|
|||||||
{ParentID: &activityID, Title: "CP配置", Code: "cp-config", Path: "/activities/cp-config", Icon: "favorite", PermissionCode: "cp-config:view", Sort: 76, Visible: true},
|
{ParentID: &activityID, Title: "CP配置", Code: "cp-config", Path: "/activities/cp-config", Icon: "favorite", PermissionCode: "cp-config:view", Sort: 76, Visible: true},
|
||||||
{ParentID: &activityID, Title: "VIP配置", Code: "vip-config", Path: "/activities/vip-config", Icon: "workspace_premium", PermissionCode: "vip-config:view", Sort: 77, Visible: true},
|
{ParentID: &activityID, Title: "VIP配置", Code: "vip-config", Path: "/activities/vip-config", Icon: "workspace_premium", PermissionCode: "vip-config:view", Sort: 77, Visible: true},
|
||||||
{ParentID: &activityID, Title: "周星配置", Code: "weekly-star", Path: "/activities/weekly-star", Icon: "star", PermissionCode: "weekly-star:view", Sort: 78, Visible: true},
|
{ParentID: &activityID, Title: "周星配置", Code: "weekly-star", Path: "/activities/weekly-star", Icon: "star", PermissionCode: "weekly-star:view", Sort: 78, Visible: true},
|
||||||
|
{ParentID: &activityID, Title: "代理开业活动", Code: "agency-opening", Path: "/activities/agency-opening", Icon: "workspace_premium", PermissionCode: "agency-opening:view", Sort: 79, Visible: true},
|
||||||
{ParentID: &gameID, Title: "游戏列表", Code: "game-list", Path: "/games", Icon: "sports_esports", PermissionCode: "game:view", Sort: 70, Visible: true},
|
{ParentID: &gameID, Title: "游戏列表", Code: "game-list", Path: "/games", Icon: "sports_esports", PermissionCode: "game:view", Sort: 70, Visible: true},
|
||||||
{ParentID: &gameID, Title: "自研游戏", Code: "self-games", Path: "/games/self-games", Icon: "settings", PermissionCode: "game:view", Sort: 80, Visible: true},
|
{ParentID: &gameID, Title: "自研游戏", Code: "self-games", Path: "/games/self-games", Icon: "settings", PermissionCode: "game:view", Sort: 80, Visible: true},
|
||||||
{ParentID: &gameID, Title: "全站机器人", Code: "game-robots", Path: "/games/robots", Icon: "team", PermissionCode: "game:view", Sort: 90, Visible: true},
|
{ParentID: &gameID, Title: "全站机器人", Code: "game-robots", Path: "/games/robots", Icon: "team", PermissionCode: "game:view", Sort: 90, Visible: true},
|
||||||
@ -530,7 +537,7 @@ func defaultRolePermissionCodes(code string) []string {
|
|||||||
"agency:view", "agency:create", "agency:status", "agency:delete",
|
"agency:view", "agency:create", "agency:status", "agency:delete",
|
||||||
"bd:view", "bd:create", "bd:update",
|
"bd:view", "bd:create", "bd:update",
|
||||||
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit", "coin-seller:exchange-rate",
|
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit", "coin-seller:exchange-rate",
|
||||||
"coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "payment-bill:view", "payment-third-party:view", "payment-third-party:update", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
|
"coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "full-server-notice:view", "full-server-notice:send", "payment-bill:view", "payment-third-party:view", "payment-third-party:update", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
|
||||||
"lucky-gift:view", "lucky-gift:update",
|
"lucky-gift:view", "lucky-gift:update",
|
||||||
"game:view", "game:create", "game:update", "game:status", "game:delete",
|
"game:view", "game:create", "game:update", "game:status", "game:delete",
|
||||||
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
|
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
|
||||||
@ -541,12 +548,13 @@ func defaultRolePermissionCodes(code string) []string {
|
|||||||
"cp-config:view", "cp-config:update",
|
"cp-config:view", "cp-config:update",
|
||||||
"vip-config:view", "vip-config:update", "vip-config:grant",
|
"vip-config:view", "vip-config:update", "vip-config:grant",
|
||||||
"weekly-star:view", "weekly-star:create", "weekly-star:update", "weekly-star:settle",
|
"weekly-star:view", "weekly-star:create", "weekly-star:update", "weekly-star:settle",
|
||||||
|
"agency-opening:view", "agency-opening:create", "agency-opening:update",
|
||||||
"log:view",
|
"log:view",
|
||||||
"job:view", "job:cancel", "export:create",
|
"job:view", "job:cancel", "export:create",
|
||||||
"upload:create",
|
"upload:create",
|
||||||
}
|
}
|
||||||
case "auditor":
|
case "auditor":
|
||||||
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "pretty-id:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "lucky-gift:view", "payment-bill:view", "payment-third-party:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "vip-config:view", "weekly-star:view", "role:view", "permission:view", "job:view"}
|
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "pretty-id:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "full-server-notice:view", "lucky-gift:view", "payment-bill:view", "payment-third-party:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "vip-config:view", "weekly-star:view", "agency-opening:view", "role:view", "permission:view", "job:view"}
|
||||||
case "readonly":
|
case "readonly":
|
||||||
return []string{
|
return []string{
|
||||||
"overview:view",
|
"overview:view",
|
||||||
@ -581,6 +589,7 @@ func defaultRolePermissionCodes(code string) []string {
|
|||||||
"coin-adjustment:view",
|
"coin-adjustment:view",
|
||||||
"report:view",
|
"report:view",
|
||||||
"gift-diamond:view",
|
"gift-diamond:view",
|
||||||
|
"full-server-notice:view",
|
||||||
"lucky-gift:view",
|
"lucky-gift:view",
|
||||||
"payment-bill:view",
|
"payment-bill:view",
|
||||||
"payment-third-party:view",
|
"payment-third-party:view",
|
||||||
@ -594,6 +603,7 @@ func defaultRolePermissionCodes(code string) []string {
|
|||||||
"cp-config:view",
|
"cp-config:view",
|
||||||
"vip-config:view",
|
"vip-config:view",
|
||||||
"weekly-star:view",
|
"weekly-star:view",
|
||||||
|
"agency-opening:view",
|
||||||
"role:view",
|
"role:view",
|
||||||
"permission:view",
|
"permission:view",
|
||||||
"menu:view",
|
"menu:view",
|
||||||
@ -627,7 +637,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
|||||||
"region:view", "region:create", "region:update", "region:status",
|
"region:view", "region:create", "region:update", "region:status",
|
||||||
"host-agency-policy:view", "host-agency-policy:create", "host-agency-policy:update", "host-agency-policy:delete", "host-agency-policy:publish", "team-salary-policy:view", "team-salary-policy:create", "team-salary-policy:update", "team-salary-policy:delete", "host-salary-settlement:view", "host-salary-settlement:settle",
|
"host-agency-policy:view", "host-agency-policy:create", "host-agency-policy:update", "host-agency-policy:delete", "host-agency-policy:publish", "team-salary-policy:view", "team-salary-policy:create", "team-salary-policy:update", "team-salary-policy:delete", "host-salary-settlement:view", "host-salary-settlement:settle",
|
||||||
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit", "coin-seller:exchange-rate",
|
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit", "coin-seller:exchange-rate",
|
||||||
"coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "payment-bill:view", "payment-third-party:view", "payment-third-party:update", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
|
"coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "full-server-notice:view", "full-server-notice:send", "payment-bill:view", "payment-third-party:view", "payment-third-party:update", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
|
||||||
"lucky-gift:view", "lucky-gift:update",
|
"lucky-gift:view", "lucky-gift:update",
|
||||||
"game:view", "game:create", "game:update", "game:status", "game:delete",
|
"game:view", "game:create", "game:update", "game:status", "game:delete",
|
||||||
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
|
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
|
||||||
@ -638,9 +648,10 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
|||||||
"cp-config:view", "cp-config:update",
|
"cp-config:view", "cp-config:update",
|
||||||
"vip-config:view", "vip-config:update", "vip-config:grant",
|
"vip-config:view", "vip-config:update", "vip-config:grant",
|
||||||
"weekly-star:view", "weekly-star:create", "weekly-star:update", "weekly-star:settle",
|
"weekly-star:view", "weekly-star:create", "weekly-star:update", "weekly-star:settle",
|
||||||
|
"agency-opening:view", "agency-opening:create", "agency-opening:update",
|
||||||
}
|
}
|
||||||
case "auditor", "readonly":
|
case "auditor", "readonly":
|
||||||
return []string{"level-config:view", "pretty-id:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-seller:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "lucky-gift:view", "payment-bill:view", "payment-third-party:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "vip-config:view", "weekly-star:view"}
|
return []string{"level-config:view", "pretty-id:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-seller:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "full-server-notice:view", "lucky-gift:view", "payment-bill:view", "payment-third-party:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "vip-config:view", "weekly-star:view", "agency-opening:view"}
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"hyapp-admin-server/internal/middleware"
|
"hyapp-admin-server/internal/middleware"
|
||||||
"hyapp-admin-server/internal/modules/achievementconfig"
|
"hyapp-admin-server/internal/modules/achievementconfig"
|
||||||
"hyapp-admin-server/internal/modules/adminuser"
|
"hyapp-admin-server/internal/modules/adminuser"
|
||||||
|
"hyapp-admin-server/internal/modules/agencyopening"
|
||||||
"hyapp-admin-server/internal/modules/appconfig"
|
"hyapp-admin-server/internal/modules/appconfig"
|
||||||
"hyapp-admin-server/internal/modules/appregistry"
|
"hyapp-admin-server/internal/modules/appregistry"
|
||||||
"hyapp-admin-server/internal/modules/appuser"
|
"hyapp-admin-server/internal/modules/appuser"
|
||||||
@ -17,6 +18,7 @@ import (
|
|||||||
"hyapp-admin-server/internal/modules/dailytask"
|
"hyapp-admin-server/internal/modules/dailytask"
|
||||||
"hyapp-admin-server/internal/modules/dashboard"
|
"hyapp-admin-server/internal/modules/dashboard"
|
||||||
"hyapp-admin-server/internal/modules/firstrechargereward"
|
"hyapp-admin-server/internal/modules/firstrechargereward"
|
||||||
|
"hyapp-admin-server/internal/modules/fullservernotice"
|
||||||
gamemanagement "hyapp-admin-server/internal/modules/gamemanagement"
|
gamemanagement "hyapp-admin-server/internal/modules/gamemanagement"
|
||||||
"hyapp-admin-server/internal/modules/giftdiamond"
|
"hyapp-admin-server/internal/modules/giftdiamond"
|
||||||
"hyapp-admin-server/internal/modules/health"
|
"hyapp-admin-server/internal/modules/health"
|
||||||
@ -55,6 +57,7 @@ import (
|
|||||||
|
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
AdminUser *adminuser.Handler
|
AdminUser *adminuser.Handler
|
||||||
|
AgencyOpening *agencyopening.Handler
|
||||||
AchievementConfig *achievementconfig.Handler
|
AchievementConfig *achievementconfig.Handler
|
||||||
AppConfig *appconfig.Handler
|
AppConfig *appconfig.Handler
|
||||||
AppRegistry *appregistry.Handler
|
AppRegistry *appregistry.Handler
|
||||||
@ -68,6 +71,7 @@ type Handlers struct {
|
|||||||
DailyTask *dailytask.Handler
|
DailyTask *dailytask.Handler
|
||||||
Dashboard *dashboard.Handler
|
Dashboard *dashboard.Handler
|
||||||
FirstRechargeReward *firstrechargereward.Handler
|
FirstRechargeReward *firstrechargereward.Handler
|
||||||
|
FullServerNotice *fullservernotice.Handler
|
||||||
Game *gamemanagement.Handler
|
Game *gamemanagement.Handler
|
||||||
GiftDiamond *giftdiamond.Handler
|
GiftDiamond *giftdiamond.Handler
|
||||||
Health *health.Handler
|
Health *health.Handler
|
||||||
@ -112,6 +116,7 @@ func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine {
|
|||||||
protected.Use(middleware.AuthRequired(auth), middleware.Audit(h.Audit))
|
protected.Use(middleware.AuthRequired(auth), middleware.Audit(h.Audit))
|
||||||
|
|
||||||
authroutes.RegisterRoutes(api, protected, h.Auth)
|
authroutes.RegisterRoutes(api, protected, h.Auth)
|
||||||
|
agencyopening.RegisterRoutes(protected, h.AgencyOpening)
|
||||||
achievementconfig.RegisterRoutes(protected, h.AchievementConfig)
|
achievementconfig.RegisterRoutes(protected, h.AchievementConfig)
|
||||||
coinledger.RegisterRoutes(protected, h.CoinLedger)
|
coinledger.RegisterRoutes(protected, h.CoinLedger)
|
||||||
menu.RegisterRoutes(protected, h.Menu)
|
menu.RegisterRoutes(protected, h.Menu)
|
||||||
@ -128,6 +133,7 @@ func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine {
|
|||||||
cumulativerechargereward.RegisterRoutes(protected, h.CumulativeRecharge)
|
cumulativerechargereward.RegisterRoutes(protected, h.CumulativeRecharge)
|
||||||
dailytask.RegisterRoutes(protected, h.DailyTask)
|
dailytask.RegisterRoutes(protected, h.DailyTask)
|
||||||
firstrechargereward.RegisterRoutes(protected, h.FirstRechargeReward)
|
firstrechargereward.RegisterRoutes(protected, h.FirstRechargeReward)
|
||||||
|
fullservernotice.RegisterRoutes(protected, h.FullServerNotice)
|
||||||
registrationreward.RegisterRoutes(protected, h.RegistrationReward)
|
registrationreward.RegisterRoutes(protected, h.RegistrationReward)
|
||||||
regionblock.RegisterRoutes(protected, h.RegionBlock)
|
regionblock.RegisterRoutes(protected, h.RegionBlock)
|
||||||
gamemanagement.RegisterRoutes(protected, h.Game)
|
gamemanagement.RegisterRoutes(protected, h.Game)
|
||||||
|
|||||||
@ -1283,6 +1283,125 @@ CREATE TABLE IF NOT EXISTS weekly_star_settlements (
|
|||||||
KEY idx_weekly_star_settlement_cycle (app_code, cycle_id, status)
|
KEY idx_weekly_star_settlement_cycle (app_code, cycle_id, status)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='周星 Top 奖励结算';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='周星 Top 奖励结算';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cp_weekly_rank_configs (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
activity_code VARCHAR(64) NOT NULL DEFAULT 'cp_weekly_rank' COMMENT '活动编码',
|
||||||
|
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||||
|
relation_type VARCHAR(32) NOT NULL DEFAULT 'cp' COMMENT '关系类型,当前固定 cp',
|
||||||
|
top_count INT NOT NULL DEFAULT 3 COMMENT '结算 Top 数量',
|
||||||
|
updated_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',
|
||||||
|
PRIMARY KEY (app_code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='CP 周榜配置表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cp_weekly_rank_rewards (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
rank_no INT NOT NULL COMMENT '名次',
|
||||||
|
resource_group_id BIGINT NOT NULL COMMENT '奖励资源组 ID',
|
||||||
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (app_code, rank_no)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='CP 周榜名次奖励资源组表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cp_weekly_rank_settlements (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
settlement_id VARCHAR(96) NOT NULL COMMENT '发奖记录 ID',
|
||||||
|
period_start_ms BIGINT NOT NULL COMMENT '周周期开始,UTC epoch ms',
|
||||||
|
period_end_ms BIGINT NOT NULL COMMENT '周周期结束,UTC epoch ms',
|
||||||
|
rank_no INT NOT NULL COMMENT '名次',
|
||||||
|
relationship_id VARCHAR(128) NOT NULL COMMENT '获奖 CP 关系 ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '获奖用户 ID',
|
||||||
|
score BIGINT NOT NULL DEFAULT 0 COMMENT '关系周榜分数',
|
||||||
|
resource_group_id BIGINT NOT NULL COMMENT '发放资源组 ID',
|
||||||
|
wallet_command_id VARCHAR(192) NOT NULL COMMENT 'wallet 发奖幂等键',
|
||||||
|
wallet_grant_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'wallet grant ID',
|
||||||
|
status VARCHAR(32) NOT NULL COMMENT 'pending/running/granted/failed',
|
||||||
|
failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||||
|
attempt_count 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, settlement_id),
|
||||||
|
UNIQUE KEY uk_cp_weekly_rank_period_user (app_code, period_start_ms, rank_no, relationship_id, user_id),
|
||||||
|
UNIQUE KEY uk_cp_weekly_rank_wallet_command (app_code, wallet_command_id),
|
||||||
|
KEY idx_cp_weekly_rank_pending (app_code, period_start_ms, period_end_ms, status, updated_at_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='CP 周榜发奖事实表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS agency_opening_cycles (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||||
|
activity_code VARCHAR(64) NOT NULL DEFAULT 'agency_opening' COMMENT '活动编码',
|
||||||
|
title VARCHAR(128) NOT NULL DEFAULT '' COMMENT '周期标题',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled/settling/settled',
|
||||||
|
start_ms BIGINT NOT NULL COMMENT 'UTC epoch ms 开始,包含',
|
||||||
|
end_ms BIGINT NOT NULL COMMENT 'UTC epoch ms 结束,不包含',
|
||||||
|
min_host_count INT NOT NULL DEFAULT 10 COMMENT '申请要求的有效主播数',
|
||||||
|
max_agency_age_days INT NOT NULL DEFAULT 7 COMMENT '代理创建后允许申请的最大天数,0 表示不限制',
|
||||||
|
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建管理员',
|
||||||
|
updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新管理员',
|
||||||
|
settled_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '结算完成时间',
|
||||||
|
locked_by VARCHAR(96) NOT NULL DEFAULT '' COMMENT '结算 worker 锁',
|
||||||
|
lock_until_ms 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',
|
||||||
|
PRIMARY KEY (app_code, cycle_id),
|
||||||
|
KEY idx_agency_opening_cycle_current (app_code, activity_code, status, start_ms, end_ms),
|
||||||
|
KEY idx_agency_opening_cycle_due (app_code, activity_code, status, end_ms, lock_until_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业活动周期';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS agency_opening_cycle_rewards (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||||
|
rank_no INT NOT NULL COMMENT '榜单名次',
|
||||||
|
reward_coin_amount 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',
|
||||||
|
PRIMARY KEY (app_code, cycle_id, rank_no)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业榜单奖励';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS agency_opening_applications (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
application_id VARCHAR(96) NOT NULL COMMENT '申请 ID',
|
||||||
|
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||||
|
agency_id BIGINT NOT NULL COMMENT '代理 ID',
|
||||||
|
agency_owner_user_id BIGINT NOT NULL COMMENT '代理 owner 用户 ID',
|
||||||
|
agency_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '代理名称快照',
|
||||||
|
host_count INT NOT NULL DEFAULT 0 COMMENT '申请时有效主播数快照',
|
||||||
|
agency_created_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '代理创建时间快照',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'applied' COMMENT 'applied/pending/granted/failed',
|
||||||
|
score_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '周期内开业流水金币',
|
||||||
|
reward_rank_no INT NOT NULL DEFAULT 0 COMMENT '结算榜单名次',
|
||||||
|
reward_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '结算奖励金币',
|
||||||
|
wallet_command_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '钱包幂等命令',
|
||||||
|
wallet_transaction_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
|
||||||
|
failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||||
|
applied_at_ms BIGINT NOT NULL COMMENT '申请时间',
|
||||||
|
granted_at_ms 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',
|
||||||
|
PRIMARY KEY (app_code, application_id),
|
||||||
|
UNIQUE KEY uk_agency_opening_owner_once (app_code, agency_owner_user_id),
|
||||||
|
KEY idx_agency_opening_wallet_command (app_code, wallet_command_id),
|
||||||
|
KEY idx_agency_opening_application_owner (app_code, agency_owner_user_id, applied_at_ms),
|
||||||
|
KEY idx_agency_opening_agency_cycle (app_code, cycle_id, agency_id),
|
||||||
|
KEY idx_agency_opening_application_rank (app_code, cycle_id, score_coin_amount, applied_at_ms, agency_id),
|
||||||
|
KEY idx_agency_opening_application_status (app_code, cycle_id, status, updated_at_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业申请和结算事实';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS agency_opening_score_events (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
source_event_id VARCHAR(128) NOT NULL COMMENT 'room-service outbox event_id',
|
||||||
|
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||||
|
application_id VARCHAR(96) NOT NULL COMMENT '命中的申请 ID',
|
||||||
|
agency_id BIGINT NOT NULL COMMENT '代理 ID',
|
||||||
|
target_user_id BIGINT NOT NULL COMMENT '收礼主播用户 ID',
|
||||||
|
score_delta BIGINT NOT NULL COMMENT '本次流水金币',
|
||||||
|
occurred_at_ms BIGINT NOT NULL COMMENT '事件发生时间,UTC epoch ms',
|
||||||
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (app_code, source_event_id),
|
||||||
|
KEY idx_agency_opening_score_event_application (app_code, application_id, occurred_at_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业流水事件幂等表';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS invite_activity_reward_configs (
|
CREATE TABLE IF NOT EXISTS invite_activity_reward_configs (
|
||||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否启用邀请活动',
|
enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否启用邀请活动',
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS cp_weekly_rank_configs (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
activity_code VARCHAR(64) NOT NULL DEFAULT 'cp_weekly_rank' COMMENT '活动编码',
|
||||||
|
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||||
|
relation_type VARCHAR(32) NOT NULL DEFAULT 'cp' COMMENT '关系类型,当前固定 cp',
|
||||||
|
top_count INT NOT NULL DEFAULT 3 COMMENT '结算 Top 数量',
|
||||||
|
updated_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',
|
||||||
|
PRIMARY KEY (app_code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='CP 周榜配置表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cp_weekly_rank_rewards (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
rank_no INT NOT NULL COMMENT '名次',
|
||||||
|
resource_group_id BIGINT NOT NULL COMMENT '奖励资源组 ID',
|
||||||
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (app_code, rank_no)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='CP 周榜名次奖励资源组表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cp_weekly_rank_settlements (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
settlement_id VARCHAR(96) NOT NULL COMMENT '发奖记录 ID',
|
||||||
|
period_start_ms BIGINT NOT NULL COMMENT '周周期开始,UTC epoch ms',
|
||||||
|
period_end_ms BIGINT NOT NULL COMMENT '周周期结束,UTC epoch ms',
|
||||||
|
rank_no INT NOT NULL COMMENT '名次',
|
||||||
|
relationship_id VARCHAR(128) NOT NULL COMMENT '获奖 CP 关系 ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '获奖用户 ID',
|
||||||
|
score BIGINT NOT NULL DEFAULT 0 COMMENT '关系周榜分数',
|
||||||
|
resource_group_id BIGINT NOT NULL COMMENT '发放资源组 ID',
|
||||||
|
wallet_command_id VARCHAR(192) NOT NULL COMMENT 'wallet 发奖幂等键',
|
||||||
|
wallet_grant_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'wallet grant ID',
|
||||||
|
status VARCHAR(32) NOT NULL COMMENT 'pending/running/granted/failed',
|
||||||
|
failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||||
|
attempt_count 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, settlement_id),
|
||||||
|
UNIQUE KEY uk_cp_weekly_rank_period_user (app_code, period_start_ms, rank_no, relationship_id, user_id),
|
||||||
|
UNIQUE KEY uk_cp_weekly_rank_wallet_command (app_code, wallet_command_id),
|
||||||
|
KEY idx_cp_weekly_rank_pending (app_code, period_start_ms, period_end_ms, status, updated_at_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='CP 周榜发奖事实表';
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS agency_opening_cycles (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||||
|
activity_code VARCHAR(64) NOT NULL DEFAULT 'agency_opening' COMMENT '活动编码',
|
||||||
|
title VARCHAR(128) NOT NULL DEFAULT '' COMMENT '周期标题',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled/settling/settled',
|
||||||
|
start_ms BIGINT NOT NULL COMMENT 'UTC epoch ms 开始,包含',
|
||||||
|
end_ms BIGINT NOT NULL COMMENT 'UTC epoch ms 结束,不包含',
|
||||||
|
min_host_count INT NOT NULL DEFAULT 10 COMMENT '申请要求的有效主播数',
|
||||||
|
max_agency_age_days INT NOT NULL DEFAULT 7 COMMENT '代理创建后允许申请的最大天数,0 表示不限制',
|
||||||
|
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建管理员',
|
||||||
|
updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新管理员',
|
||||||
|
settled_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '结算完成时间',
|
||||||
|
locked_by VARCHAR(96) NOT NULL DEFAULT '' COMMENT '结算 worker 锁',
|
||||||
|
lock_until_ms 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',
|
||||||
|
PRIMARY KEY (app_code, cycle_id),
|
||||||
|
KEY idx_agency_opening_cycle_current (app_code, activity_code, status, start_ms, end_ms),
|
||||||
|
KEY idx_agency_opening_cycle_due (app_code, activity_code, status, end_ms, lock_until_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业活动周期';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS agency_opening_cycle_rewards (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||||
|
rank_no INT NOT NULL COMMENT '档位序号',
|
||||||
|
threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '命中该档需要的代理房间流水金币',
|
||||||
|
reward_coin_amount 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',
|
||||||
|
PRIMARY KEY (app_code, cycle_id, rank_no)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业流水奖励档位';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS agency_opening_applications (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
application_id VARCHAR(96) NOT NULL COMMENT '申请 ID',
|
||||||
|
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||||
|
agency_id BIGINT NOT NULL COMMENT '代理 ID',
|
||||||
|
agency_owner_user_id BIGINT NOT NULL COMMENT '代理 owner 用户 ID',
|
||||||
|
agency_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '代理名称快照',
|
||||||
|
host_count INT NOT NULL DEFAULT 0 COMMENT '申请时有效主播数快照',
|
||||||
|
agency_created_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '代理创建时间快照',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'applied' COMMENT 'applied/pending/granted/failed',
|
||||||
|
score_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '周期内开业流水金币',
|
||||||
|
reward_rank_no INT NOT NULL DEFAULT 0 COMMENT '结算命中的档位序号',
|
||||||
|
reward_threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '结算命中的流水阈值',
|
||||||
|
reward_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '结算奖励金币',
|
||||||
|
wallet_command_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '钱包幂等命令',
|
||||||
|
wallet_transaction_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
|
||||||
|
failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||||
|
applied_at_ms BIGINT NOT NULL COMMENT '申请时间',
|
||||||
|
granted_at_ms 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',
|
||||||
|
PRIMARY KEY (app_code, application_id),
|
||||||
|
UNIQUE KEY uk_agency_opening_owner_once (app_code, agency_owner_user_id),
|
||||||
|
KEY idx_agency_opening_wallet_command (app_code, wallet_command_id),
|
||||||
|
KEY idx_agency_opening_application_owner (app_code, agency_owner_user_id, applied_at_ms),
|
||||||
|
KEY idx_agency_opening_agency_cycle (app_code, cycle_id, agency_id),
|
||||||
|
KEY idx_agency_opening_application_rank (app_code, cycle_id, score_coin_amount, applied_at_ms, agency_id),
|
||||||
|
KEY idx_agency_opening_application_status (app_code, cycle_id, status, updated_at_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业申请和结算事实';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS agency_opening_score_events (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
source_event_id VARCHAR(128) NOT NULL COMMENT 'room-service outbox event_id',
|
||||||
|
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||||
|
application_id VARCHAR(96) NOT NULL COMMENT '命中的申请 ID',
|
||||||
|
agency_id BIGINT NOT NULL COMMENT '代理 ID',
|
||||||
|
target_user_id BIGINT NOT NULL COMMENT '收礼主播用户 ID',
|
||||||
|
score_delta BIGINT NOT NULL COMMENT '本次流水金币',
|
||||||
|
occurred_at_ms BIGINT NOT NULL COMMENT '事件发生时间,UTC epoch ms',
|
||||||
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (app_code, source_event_id),
|
||||||
|
KEY idx_agency_opening_score_event_application (app_code, application_id, occurred_at_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业流水事件幂等表';
|
||||||
@ -11,7 +11,7 @@ func registerGRPCServers(server *grpc.Server, services *serviceBundle) {
|
|||||||
activityv1.RegisterMessageInboxServiceServer(server, grpcserver.NewMessageServer(services.message))
|
activityv1.RegisterMessageInboxServiceServer(server, grpcserver.NewMessageServer(services.message))
|
||||||
// cron 入口只做调度适配;具体状态机仍在各 activity service 内部。
|
// cron 入口只做调度适配;具体状态机仍在各 activity service 内部。
|
||||||
// weekly-star 和 room-turnover 都依赖 wallet 发奖,所以这里必须把同一批已装配的 service 注入 cron server。
|
// weekly-star 和 room-turnover 都依赖 wallet 发奖,所以这里必须把同一批已装配的 service 注入 cron server。
|
||||||
activityv1.RegisterActivityCronServiceServer(server, grpcserver.NewCronServer(services.message, services.growth, services.achievement, services.weeklyStar, services.roomTurnoverReward))
|
activityv1.RegisterActivityCronServiceServer(server, grpcserver.NewCronServer(services.message, services.growth, services.achievement, services.weeklyStar, services.roomTurnoverReward, services.agencyOpening, services.cpWeeklyRank))
|
||||||
activityv1.RegisterTaskServiceServer(server, grpcserver.NewTaskServer(services.task))
|
activityv1.RegisterTaskServiceServer(server, grpcserver.NewTaskServer(services.task))
|
||||||
activityv1.RegisterAdminTaskServiceServer(server, grpcserver.NewAdminTaskServer(services.task))
|
activityv1.RegisterAdminTaskServiceServer(server, grpcserver.NewAdminTaskServer(services.task))
|
||||||
activityv1.RegisterGrowthLevelServiceServer(server, grpcserver.NewGrowthLevelServer(services.growth))
|
activityv1.RegisterGrowthLevelServiceServer(server, grpcserver.NewGrowthLevelServer(services.growth))
|
||||||
@ -26,11 +26,16 @@ func registerGRPCServers(server *grpc.Server, services *serviceBundle) {
|
|||||||
activityv1.RegisterAdminSevenDayCheckInServiceServer(server, grpcserver.NewAdminSevenDayCheckInServer(services.sevenDayCheckIn))
|
activityv1.RegisterAdminSevenDayCheckInServiceServer(server, grpcserver.NewAdminSevenDayCheckInServer(services.sevenDayCheckIn))
|
||||||
activityv1.RegisterWeeklyStarServiceServer(server, grpcserver.NewWeeklyStarServer(services.weeklyStar))
|
activityv1.RegisterWeeklyStarServiceServer(server, grpcserver.NewWeeklyStarServer(services.weeklyStar))
|
||||||
activityv1.RegisterAdminWeeklyStarServiceServer(server, grpcserver.NewAdminWeeklyStarServer(services.weeklyStar))
|
activityv1.RegisterAdminWeeklyStarServiceServer(server, grpcserver.NewAdminWeeklyStarServer(services.weeklyStar))
|
||||||
|
cpWeeklyRankServer := grpcserver.NewCPWeeklyRankServer(services.cpWeeklyRank)
|
||||||
|
activityv1.RegisterCPWeeklyRankServiceServer(server, cpWeeklyRankServer)
|
||||||
|
activityv1.RegisterAdminCPWeeklyRankServiceServer(server, cpWeeklyRankServer)
|
||||||
|
activityv1.RegisterAgencyOpeningServiceServer(server, grpcserver.NewAgencyOpeningServer(services.agencyOpening))
|
||||||
|
activityv1.RegisterAdminAgencyOpeningServiceServer(server, grpcserver.NewAdminAgencyOpeningServer(services.agencyOpening))
|
||||||
activityv1.RegisterRoomTurnoverRewardServiceServer(server, grpcserver.NewRoomTurnoverRewardServer(services.roomTurnoverReward))
|
activityv1.RegisterRoomTurnoverRewardServiceServer(server, grpcserver.NewRoomTurnoverRewardServer(services.roomTurnoverReward))
|
||||||
activityv1.RegisterAdminRoomTurnoverRewardServiceServer(server, grpcserver.NewAdminRoomTurnoverRewardServer(services.roomTurnoverReward))
|
activityv1.RegisterAdminRoomTurnoverRewardServiceServer(server, grpcserver.NewAdminRoomTurnoverRewardServer(services.roomTurnoverReward))
|
||||||
// BroadcastServer 同时承载 BroadcastService 和 RoomEventConsumerService。
|
// BroadcastServer 同时承载 BroadcastService 和 RoomEventConsumerService。
|
||||||
// gRPC 直连投递和 MQ 消费都必须经过相同的 room event fanout 顺序,否则本地验证会通过但线上 MQ 路径漏积分。
|
// gRPC 直连投递和 MQ 消费都必须经过相同的 room event fanout 顺序,否则本地验证会通过但线上 MQ 路径漏积分。
|
||||||
broadcastServer := grpcserver.NewBroadcastServer(services.broadcast, services.growth, services.weeklyStar, services.task, services.roomTurnoverReward)
|
broadcastServer := grpcserver.NewBroadcastServer(services.broadcast, services.growth, services.weeklyStar, services.task, services.roomTurnoverReward, services.agencyOpening)
|
||||||
activityv1.RegisterBroadcastServiceServer(server, broadcastServer)
|
activityv1.RegisterBroadcastServiceServer(server, broadcastServer)
|
||||||
activityv1.RegisterRoomEventConsumerServiceServer(server, broadcastServer)
|
activityv1.RegisterRoomEventConsumerServiceServer(server, broadcastServer)
|
||||||
activityv1.RegisterFirstRechargeRewardServiceServer(server, grpcserver.NewFirstRechargeRewardServer(services.firstRechargeReward))
|
activityv1.RegisterFirstRechargeRewardServiceServer(server, grpcserver.NewFirstRechargeRewardServer(services.firstRechargeReward))
|
||||||
|
|||||||
@ -120,6 +120,9 @@ func newRoomOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketm
|
|||||||
if _, err := services.weeklyStar.HandleRoomEvent(eventCtx, envelope); err != nil {
|
if _, err := services.weeklyStar.HandleRoomEvent(eventCtx, envelope); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if _, err := services.agencyOpening.HandleRoomEvent(eventCtx, envelope); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if _, err := services.task.HandleRoomEvent(eventCtx, envelope); err != nil {
|
if _, err := services.task.HandleRoomEvent(eventCtx, envelope); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,9 @@ import (
|
|||||||
"hyapp/services/activity-service/internal/config"
|
"hyapp/services/activity-service/internal/config"
|
||||||
achievementservice "hyapp/services/activity-service/internal/service/achievement"
|
achievementservice "hyapp/services/activity-service/internal/service/achievement"
|
||||||
activityservice "hyapp/services/activity-service/internal/service/activity"
|
activityservice "hyapp/services/activity-service/internal/service/activity"
|
||||||
|
agencyopeningservice "hyapp/services/activity-service/internal/service/agencyopening"
|
||||||
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
||||||
|
cpweeklyrankservice "hyapp/services/activity-service/internal/service/cpweeklyrank"
|
||||||
cumulativerechargeservice "hyapp/services/activity-service/internal/service/cumulativerecharge"
|
cumulativerechargeservice "hyapp/services/activity-service/internal/service/cumulativerecharge"
|
||||||
firstrechargeservice "hyapp/services/activity-service/internal/service/firstrecharge"
|
firstrechargeservice "hyapp/services/activity-service/internal/service/firstrecharge"
|
||||||
growthservice "hyapp/services/activity-service/internal/service/growth"
|
growthservice "hyapp/services/activity-service/internal/service/growth"
|
||||||
@ -33,7 +35,9 @@ type serviceBundle struct {
|
|||||||
sevenDayCheckIn *sevendaycheckinservice.Service
|
sevenDayCheckIn *sevendaycheckinservice.Service
|
||||||
growth *growthservice.Service
|
growth *growthservice.Service
|
||||||
achievement *achievementservice.Service
|
achievement *achievementservice.Service
|
||||||
|
agencyOpening *agencyopeningservice.Service
|
||||||
weeklyStar *weeklystarservice.Service
|
weeklyStar *weeklystarservice.Service
|
||||||
|
cpWeeklyRank *cpweeklyrankservice.Service
|
||||||
roomTurnoverReward *roomturnoverrewardservice.Service
|
roomTurnoverReward *roomturnoverrewardservice.Service
|
||||||
broadcast *broadcastservice.Service
|
broadcast *broadcastservice.Service
|
||||||
luckyGift *luckygiftservice.Service
|
luckyGift *luckygiftservice.Service
|
||||||
@ -54,7 +58,9 @@ func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository,
|
|||||||
sevenDayCheckInSvc := sevendaycheckinservice.New(repository, walletClient)
|
sevenDayCheckInSvc := sevendaycheckinservice.New(repository, walletClient)
|
||||||
growthSvc := growthservice.New(repository, walletClient)
|
growthSvc := growthservice.New(repository, walletClient)
|
||||||
achievementSvc := achievementservice.New(repository, walletClient)
|
achievementSvc := achievementservice.New(repository, walletClient)
|
||||||
|
agencyOpeningSvc := agencyopeningservice.New(repository, walletClient, activityclient.NewGRPCAgencyOpeningSource(clients.userConn), roomClient)
|
||||||
weeklyStarSvc := weeklystarservice.New(repository, walletClient)
|
weeklyStarSvc := weeklystarservice.New(repository, walletClient)
|
||||||
|
cpWeeklyRankSvc := cpweeklyrankservice.New(repository, activityclient.NewGRPCCPWeeklyRankSource(clients.userConn), walletClient)
|
||||||
roomTurnoverRewardSvc := roomturnoverrewardservice.New(repository, walletClient, roomClient)
|
roomTurnoverRewardSvc := roomturnoverrewardservice.New(repository, walletClient, roomClient)
|
||||||
|
|
||||||
// Tencent IM 是区域广播、房间礼物播报和红包播报的外部发布器。
|
// Tencent IM 是区域广播、房间礼物播报和红包播报的外部发布器。
|
||||||
@ -102,7 +108,9 @@ func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository,
|
|||||||
sevenDayCheckIn: sevenDayCheckInSvc,
|
sevenDayCheckIn: sevenDayCheckInSvc,
|
||||||
growth: growthSvc,
|
growth: growthSvc,
|
||||||
achievement: achievementSvc,
|
achievement: achievementSvc,
|
||||||
|
agencyOpening: agencyOpeningSvc,
|
||||||
weeklyStar: weeklyStarSvc,
|
weeklyStar: weeklyStarSvc,
|
||||||
|
cpWeeklyRank: cpWeeklyRankSvc,
|
||||||
roomTurnoverReward: roomTurnoverRewardSvc,
|
roomTurnoverReward: roomTurnoverRewardSvc,
|
||||||
broadcast: broadcastSvc,
|
broadcast: broadcastSvc,
|
||||||
luckyGift: luckyGiftSvc,
|
luckyGift: luckyGiftSvc,
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
domain "hyapp/services/activity-service/internal/domain/agencyopening"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GRPCAgencyOpeningSource reads current Agency facts from user-service for activity qualification and scoring.
|
||||||
|
type GRPCAgencyOpeningSource struct {
|
||||||
|
client userv1.UserHostServiceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGRPCAgencyOpeningSource(conn *grpc.ClientConn) *GRPCAgencyOpeningSource {
|
||||||
|
return &GRPCAgencyOpeningSource{client: userv1.NewUserHostServiceClient(conn)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCAgencyOpeningSource) ResolveOwnerAgency(ctx context.Context, userID int64) (domain.AgencySnapshot, bool, error) {
|
||||||
|
summaryResp, err := s.client.GetUserRoleSummary(ctx, &userv1.GetUserRoleSummaryRequest{
|
||||||
|
Meta: meta(ctx, "agency-opening-owner"),
|
||||||
|
UserId: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.AgencySnapshot{}, false, err
|
||||||
|
}
|
||||||
|
summary := summaryResp.GetSummary()
|
||||||
|
if summary == nil || !summary.GetIsAgency() || summary.GetAgencyId() <= 0 {
|
||||||
|
return domain.AgencySnapshot{}, false, nil
|
||||||
|
}
|
||||||
|
agencyResp, err := s.client.GetAgency(ctx, &userv1.GetAgencyRequest{
|
||||||
|
Meta: meta(ctx, "agency-opening-agency"),
|
||||||
|
AgencyId: summary.GetAgencyId(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.AgencySnapshot{}, false, err
|
||||||
|
}
|
||||||
|
agency := agencyResp.GetAgency()
|
||||||
|
if agency == nil || agency.GetOwnerUserId() != userID {
|
||||||
|
return domain.AgencySnapshot{}, false, nil
|
||||||
|
}
|
||||||
|
snapshot := agencySnapshotFromProto(agency)
|
||||||
|
membersResp, err := s.client.GetAgencyMembers(ctx, &userv1.GetAgencyMembersRequest{
|
||||||
|
Meta: meta(ctx, "agency-opening-agency-members"),
|
||||||
|
AgencyId: snapshot.AgencyID,
|
||||||
|
Status: "active",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.AgencySnapshot{}, false, err
|
||||||
|
}
|
||||||
|
// GetAgency 的基础事实不总是携带聚合人数;资格判断必须使用当前 active membership 事实,避免把有效 agency 误判成 0 主播。
|
||||||
|
snapshot.HostCount = int32(len(membersResp.GetMemberships()))
|
||||||
|
return snapshot, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencySnapshotFromProto(agency *userv1.Agency) domain.AgencySnapshot {
|
||||||
|
if agency == nil {
|
||||||
|
return domain.AgencySnapshot{}
|
||||||
|
}
|
||||||
|
return domain.AgencySnapshot{
|
||||||
|
AgencyID: agency.GetAgencyId(),
|
||||||
|
OwnerUserID: agency.GetOwnerUserId(),
|
||||||
|
Name: agency.GetName(),
|
||||||
|
Status: agency.GetStatus(),
|
||||||
|
HostCount: agency.GetHostCount(),
|
||||||
|
AgencyCreatedAtMS: agency.GetCreatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func meta(ctx context.Context, requestID string) *userv1.RequestMeta {
|
||||||
|
return &userv1.RequestMeta{
|
||||||
|
RequestId: requestID,
|
||||||
|
Caller: "activity-service",
|
||||||
|
AppCode: appcode.FromContext(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
domain "hyapp/services/activity-service/internal/domain/cpweeklyrank"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GRPCCPWeeklyRankSource reads CP weekly rank snapshots from user-service, the owner of CP relationship facts.
|
||||||
|
type GRPCCPWeeklyRankSource struct {
|
||||||
|
client userv1.UserCPInternalServiceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGRPCCPWeeklyRankSource(conn *grpc.ClientConn) *GRPCCPWeeklyRankSource {
|
||||||
|
return &GRPCCPWeeklyRankSource{client: userv1.NewUserCPInternalServiceClient(conn)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCCPWeeklyRankSource) ListCPWeeklyRankEntries(ctx context.Context, relationType string, startMS int64, endMS int64, limit int32) ([]domain.Entry, error) {
|
||||||
|
resp, err := s.client.ListCPWeeklyRankEntries(ctx, &userv1.ListCPWeeklyRankEntriesRequest{
|
||||||
|
Meta: &userv1.RequestMeta{
|
||||||
|
RequestId: "cp-weekly-rank",
|
||||||
|
Caller: "activity-service",
|
||||||
|
AppCode: appcode.FromContext(ctx),
|
||||||
|
},
|
||||||
|
RelationType: relationType,
|
||||||
|
StartMs: startMS,
|
||||||
|
EndMs: endMS,
|
||||||
|
Limit: limit,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries := make([]domain.Entry, 0, len(resp.GetEntries()))
|
||||||
|
for _, item := range resp.GetEntries() {
|
||||||
|
entries = append(entries, domain.Entry{
|
||||||
|
Rank: item.GetRank(),
|
||||||
|
RelationshipID: item.GetRelationshipId(),
|
||||||
|
RelationType: item.GetRelationType(),
|
||||||
|
Score: item.GetScore(),
|
||||||
|
UserA: cpWeeklyRankUserFromProto(item.GetUserA()),
|
||||||
|
UserB: cpWeeklyRankUserFromProto(item.GetUserB()),
|
||||||
|
FormedAtMS: item.GetFormedAtMs(),
|
||||||
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
|
FirstScoredAtMS: item.GetFirstScoredAtMs(),
|
||||||
|
LastScoredAtMS: item.GetLastScoredAtMs(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankUserFromProto(user *userv1.CPIntimacyLeaderboardUser) domain.User {
|
||||||
|
if user == nil {
|
||||||
|
return domain.User{}
|
||||||
|
}
|
||||||
|
return domain.User{
|
||||||
|
UserID: user.GetUserId(),
|
||||||
|
DisplayUserID: user.GetDisplayUserId(),
|
||||||
|
Username: user.GetUsername(),
|
||||||
|
Avatar: user.GetAvatar(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
package agencyopening
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActivityCode = "agency_opening"
|
||||||
|
|
||||||
|
StatusDraft = "draft"
|
||||||
|
StatusActive = "active"
|
||||||
|
StatusDisabled = "disabled"
|
||||||
|
StatusSettling = "settling"
|
||||||
|
StatusSettled = "settled"
|
||||||
|
|
||||||
|
ApplicationStatusApplied = "applied"
|
||||||
|
ApplicationStatusPending = "pending"
|
||||||
|
ApplicationStatusGranted = "granted"
|
||||||
|
ApplicationStatusFailed = "failed"
|
||||||
|
|
||||||
|
EventStatusConsumed = "consumed"
|
||||||
|
EventStatusDuplicate = "duplicate"
|
||||||
|
EventStatusSkipped = "skipped"
|
||||||
|
|
||||||
|
GrantSourceAgencyOpening = "agency_opening"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reward 是代理开业流水档位;RankNo 只保留为档位展示顺序和旧字段兼容,结算命中只看 ThresholdCoinSpent。
|
||||||
|
type Reward struct {
|
||||||
|
AppCode string
|
||||||
|
CycleID string
|
||||||
|
RankNo int32
|
||||||
|
ThresholdCoinSpent int64
|
||||||
|
RewardCoinAmount int64
|
||||||
|
CreatedAtMS int64
|
||||||
|
UpdatedAtMS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle is the UTC activity window configured by admin.
|
||||||
|
type Cycle struct {
|
||||||
|
AppCode string
|
||||||
|
CycleID string
|
||||||
|
ActivityCode string
|
||||||
|
Title string
|
||||||
|
Status string
|
||||||
|
StartMS int64
|
||||||
|
EndMS int64
|
||||||
|
MinHostCount int32
|
||||||
|
MaxAgencyAgeDays int32
|
||||||
|
CreatedByAdminID int64
|
||||||
|
UpdatedByAdminID int64
|
||||||
|
SettledAtMS int64
|
||||||
|
CreatedAtMS int64
|
||||||
|
UpdatedAtMS int64
|
||||||
|
Rewards []Reward
|
||||||
|
}
|
||||||
|
|
||||||
|
// CycleCommand carries an admin mutation after transport normalization.
|
||||||
|
type CycleCommand struct {
|
||||||
|
Cycle Cycle
|
||||||
|
OperatorAdminID int64
|
||||||
|
RequireNewRecord bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListQuery filters admin list reads.
|
||||||
|
type ListQuery struct {
|
||||||
|
Status string
|
||||||
|
StartMS int64
|
||||||
|
EndMS int64
|
||||||
|
Page int32
|
||||||
|
PageSize int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application is both the submitted application record and the settlement fact.
|
||||||
|
type Application struct {
|
||||||
|
AppCode string
|
||||||
|
ApplicationID string
|
||||||
|
CycleID string
|
||||||
|
AgencyID int64
|
||||||
|
AgencyOwnerUserID int64
|
||||||
|
AgencyName string
|
||||||
|
HostCount int32
|
||||||
|
AgencyCreatedAtMS int64
|
||||||
|
Status string
|
||||||
|
ScoreCoinAmount int64
|
||||||
|
RewardRankNo int32
|
||||||
|
RewardThresholdCoin int64
|
||||||
|
RewardCoinAmount int64
|
||||||
|
WalletCommandID string
|
||||||
|
WalletTransactionID string
|
||||||
|
FailureReason string
|
||||||
|
AppliedAtMS int64
|
||||||
|
GrantedAtMS int64
|
||||||
|
CreatedAtMS int64
|
||||||
|
UpdatedAtMS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplicationQuery filters admin and H5 leaderboard reads.
|
||||||
|
type ApplicationQuery struct {
|
||||||
|
CycleID string
|
||||||
|
Status string
|
||||||
|
AgencyID int64
|
||||||
|
AgencyOwnerUserID int64
|
||||||
|
Page int32
|
||||||
|
PageSize int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgencySnapshot is read from user-service at apply time so the activity record remains auditable.
|
||||||
|
type AgencySnapshot struct {
|
||||||
|
AgencyID int64
|
||||||
|
OwnerUserID int64
|
||||||
|
Name string
|
||||||
|
Status string
|
||||||
|
HostCount int32
|
||||||
|
AgencyCreatedAtMS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qualification combines the current cycle, agency snapshot and application state for H5.
|
||||||
|
type Qualification struct {
|
||||||
|
Cycle Cycle
|
||||||
|
Application Application
|
||||||
|
Agency AgencySnapshot
|
||||||
|
Enabled bool
|
||||||
|
Eligible bool
|
||||||
|
Joined bool
|
||||||
|
IneligibleReason string
|
||||||
|
ServerTimeMS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GiftEvent is the room-service gift fact projected into the applicant room owner's opening revenue.
|
||||||
|
type GiftEvent struct {
|
||||||
|
EventID string
|
||||||
|
RoomID string
|
||||||
|
RoomOwnerUserID int64
|
||||||
|
TargetUserID int64
|
||||||
|
CoinSpent int64
|
||||||
|
OccurredAtMS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventResult describes idempotent revenue consumption for one room event.
|
||||||
|
type EventResult struct {
|
||||||
|
EventID string
|
||||||
|
Status string
|
||||||
|
}
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
package cpweeklyrank
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActivityCode = "cp_weekly_rank"
|
||||||
|
|
||||||
|
SettlementStatusPending = "pending"
|
||||||
|
SettlementStatusRunning = "running"
|
||||||
|
SettlementStatusGranted = "granted"
|
||||||
|
SettlementStatusFailed = "failed"
|
||||||
|
|
||||||
|
GrantSourceCPWeeklyRank = "cp_weekly_rank"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reward maps one CP weekly rank to the resource group granted to both relationship users.
|
||||||
|
type Reward struct {
|
||||||
|
AppCode string
|
||||||
|
RankNo int32
|
||||||
|
ResourceGroupID int64
|
||||||
|
CreatedAtMS int64
|
||||||
|
UpdatedAtMS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is the activity-service owned switch and reward definition for CP weekly rank.
|
||||||
|
type Config struct {
|
||||||
|
AppCode string
|
||||||
|
ActivityCode string
|
||||||
|
Enabled bool
|
||||||
|
RelationType string
|
||||||
|
TopCount int32
|
||||||
|
UpdatedByAdminID int64
|
||||||
|
CreatedAtMS int64
|
||||||
|
UpdatedAtMS int64
|
||||||
|
Rewards []Reward
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is a user-service profile snapshot included in leaderboard reads.
|
||||||
|
type User struct {
|
||||||
|
UserID int64
|
||||||
|
DisplayUserID string
|
||||||
|
Username string
|
||||||
|
Avatar string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry is one relationship score inside a concrete UTC week window.
|
||||||
|
type Entry struct {
|
||||||
|
Rank int64
|
||||||
|
RelationshipID string
|
||||||
|
RelationType string
|
||||||
|
Score int64
|
||||||
|
UserA User
|
||||||
|
UserB User
|
||||||
|
FormedAtMS int64
|
||||||
|
UpdatedAtMS int64
|
||||||
|
FirstScoredAtMS int64
|
||||||
|
LastScoredAtMS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settlement is one resource-group grant job; every winning relationship creates two rows, one for each user.
|
||||||
|
type Settlement struct {
|
||||||
|
AppCode string
|
||||||
|
SettlementID string
|
||||||
|
PeriodStartMS int64
|
||||||
|
PeriodEndMS int64
|
||||||
|
RankNo int32
|
||||||
|
RelationshipID string
|
||||||
|
UserID int64
|
||||||
|
Score int64
|
||||||
|
ResourceGroupID int64
|
||||||
|
WalletCommandID string
|
||||||
|
WalletGrantID string
|
||||||
|
Status string
|
||||||
|
FailureReason string
|
||||||
|
AttemptCount int32
|
||||||
|
CreatedAtMS int64
|
||||||
|
UpdatedAtMS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status is the H5 read model for current week leaderboard and configured rewards.
|
||||||
|
type Status struct {
|
||||||
|
Config Config
|
||||||
|
Entries []Entry
|
||||||
|
PeriodStartMS int64
|
||||||
|
PeriodEndMS int64
|
||||||
|
ServerTimeMS int64
|
||||||
|
}
|
||||||
@ -0,0 +1,423 @@
|
|||||||
|
package agencyopening
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||||
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/logx"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
domain "hyapp/services/activity-service/internal/domain/agencyopening"
|
||||||
|
)
|
||||||
|
|
||||||
|
const rewardReason = "agency opening reward"
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
ListAgencyOpeningCycles(ctx context.Context, query domain.ListQuery) ([]domain.Cycle, int64, error)
|
||||||
|
GetAgencyOpeningCycle(ctx context.Context, cycleID string) (domain.Cycle, error)
|
||||||
|
UpsertAgencyOpeningCycle(ctx context.Context, command domain.CycleCommand, nowMS int64) (domain.Cycle, bool, error)
|
||||||
|
SetAgencyOpeningCycleStatus(ctx context.Context, cycleID string, status string, operatorAdminID int64, nowMS int64) (domain.Cycle, error)
|
||||||
|
FindCurrentAgencyOpeningCycle(ctx context.Context, nowMS int64) (domain.Cycle, bool, error)
|
||||||
|
GetAgencyOpeningApplicationByOwner(ctx context.Context, ownerUserID int64) (domain.Application, bool, error)
|
||||||
|
CreateAgencyOpeningApplication(ctx context.Context, cycle domain.Cycle, agency domain.AgencySnapshot, nowMS int64) (domain.Application, bool, error)
|
||||||
|
ListAgencyOpeningApplications(ctx context.Context, query domain.ApplicationQuery) ([]domain.Application, int64, error)
|
||||||
|
ConsumeAgencyOpeningGiftEvent(ctx context.Context, event domain.GiftEvent, nowMS int64) (domain.EventResult, error)
|
||||||
|
ClaimDueAgencyOpeningCycles(ctx context.Context, workerID string, nowMS int64, lockTTL time.Duration, batchSize int32) ([]domain.Cycle, error)
|
||||||
|
PrepareAgencyOpeningSettlements(ctx context.Context, cycle domain.Cycle, nowMS int64) ([]domain.Application, error)
|
||||||
|
MarkAgencyOpeningApplicationGranted(ctx context.Context, applicationID string, walletTransactionID string, grantedAtMS int64) (domain.Application, error)
|
||||||
|
MarkAgencyOpeningApplicationFailed(ctx context.Context, applicationID string, reason string, nowMS int64) error
|
||||||
|
FinishAgencyOpeningCycleIfComplete(ctx context.Context, cycleID string, nowMS int64) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgencySource interface {
|
||||||
|
ResolveOwnerAgency(ctx context.Context, userID int64) (domain.AgencySnapshot, bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalletClient interface {
|
||||||
|
CreditAgencyOpeningReward(ctx context.Context, req *walletv1.CreditAgencyOpeningRewardRequest, opts ...grpc.CallOption) (*walletv1.CreditAgencyOpeningRewardResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomQueryClient interface {
|
||||||
|
AdminGetRoom(ctx context.Context, req *roomv1.AdminGetRoomRequest, opts ...grpc.CallOption) (*roomv1.AdminGetRoomResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
repository Repository
|
||||||
|
agency AgencySource
|
||||||
|
room RoomQueryClient
|
||||||
|
wallet WalletClient
|
||||||
|
now func() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(repository Repository, wallet WalletClient, agency AgencySource, room RoomQueryClient) *Service {
|
||||||
|
return &Service{repository: repository, wallet: wallet, agency: agency, room: room, now: time.Now}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListCycles(ctx context.Context, query domain.ListQuery) ([]domain.Cycle, int64, error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
query.Status = normalizeStatus(query.Status)
|
||||||
|
return s.repository.ListAgencyOpeningCycles(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetCycle(ctx context.Context, cycleID string) (domain.Cycle, error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return domain.Cycle{}, err
|
||||||
|
}
|
||||||
|
cycleID = strings.TrimSpace(cycleID)
|
||||||
|
if cycleID == "" {
|
||||||
|
return domain.Cycle{}, xerr.New(xerr.InvalidArgument, "cycle_id is required")
|
||||||
|
}
|
||||||
|
return s.repository.GetAgencyOpeningCycle(ctx, cycleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateCycle(ctx context.Context, command domain.CycleCommand) (domain.Cycle, bool, error) {
|
||||||
|
command.RequireNewRecord = true
|
||||||
|
return s.upsertCycle(ctx, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateCycle(ctx context.Context, command domain.CycleCommand) (domain.Cycle, bool, error) {
|
||||||
|
command.RequireNewRecord = false
|
||||||
|
return s.upsertCycle(ctx, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetCycleStatus(ctx context.Context, cycleID string, status string, operatorAdminID int64) (domain.Cycle, error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return domain.Cycle{}, err
|
||||||
|
}
|
||||||
|
status = normalizeStatus(status)
|
||||||
|
if !validStatus(status) || status == domain.StatusSettling {
|
||||||
|
return domain.Cycle{}, xerr.New(xerr.InvalidArgument, "status is invalid")
|
||||||
|
}
|
||||||
|
if operatorAdminID <= 0 {
|
||||||
|
return domain.Cycle{}, xerr.New(xerr.InvalidArgument, "operator_admin_id is required")
|
||||||
|
}
|
||||||
|
return s.repository.SetAgencyOpeningCycleStatus(ctx, strings.TrimSpace(cycleID), status, operatorAdminID, s.now().UTC().UnixMilli())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetStatus(ctx context.Context, userID int64) (domain.Qualification, error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return domain.Qualification{}, err
|
||||||
|
}
|
||||||
|
nowMS := s.now().UTC().UnixMilli()
|
||||||
|
cycle, exists, err := s.repository.FindCurrentAgencyOpeningCycle(ctx, nowMS)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Qualification{}, err
|
||||||
|
}
|
||||||
|
result := domain.Qualification{Cycle: cycle, Enabled: exists, ServerTimeMS: nowMS}
|
||||||
|
if !exists {
|
||||||
|
result.IneligibleReason = "activity_disabled"
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
agency, ok, err := s.resolveOwnerAgency(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Qualification{}, err
|
||||||
|
}
|
||||||
|
result.Agency = agency
|
||||||
|
if !ok {
|
||||||
|
result.IneligibleReason = "active_agency_owner_required"
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
application, joined, err := s.repository.GetAgencyOpeningApplicationByOwner(ctx, agency.OwnerUserID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Qualification{}, err
|
||||||
|
}
|
||||||
|
result.Application = application
|
||||||
|
result.Joined = joined
|
||||||
|
result.Eligible, result.IneligibleReason = eligible(cycle, agency, nowMS)
|
||||||
|
if joined {
|
||||||
|
result.Eligible = false
|
||||||
|
result.IneligibleReason = "already_applied"
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Apply(ctx context.Context, userID int64, commandID string) (domain.Application, bool, error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return domain.Application{}, false, err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(commandID) == "" {
|
||||||
|
return domain.Application{}, false, xerr.New(xerr.InvalidArgument, "command_id is required")
|
||||||
|
}
|
||||||
|
qualification, err := s.GetStatus(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Application{}, false, err
|
||||||
|
}
|
||||||
|
if !qualification.Enabled {
|
||||||
|
return domain.Application{}, false, xerr.New(xerr.Conflict, "activity is not active")
|
||||||
|
}
|
||||||
|
if qualification.Joined {
|
||||||
|
return qualification.Application, false, nil
|
||||||
|
}
|
||||||
|
if !qualification.Eligible {
|
||||||
|
return domain.Application{}, false, xerr.New(xerr.Conflict, qualification.IneligibleReason)
|
||||||
|
}
|
||||||
|
return s.repository.CreateAgencyOpeningApplication(ctx, qualification.Cycle, qualification.Agency, s.now().UTC().UnixMilli())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListApplications(ctx context.Context, query domain.ApplicationQuery) ([]domain.Application, int64, error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
query.Status = normalizeApplicationStatus(query.Status)
|
||||||
|
return s.repository.ListAgencyOpeningApplications(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) HandleGiftEvent(ctx context.Context, event domain.GiftEvent) (int32, error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if event.EventID == "" || event.RoomOwnerUserID <= 0 || event.CoinSpent <= 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
result, err := s.repository.ConsumeAgencyOpeningGiftEvent(ctx, event, s.now().UTC().UnixMilli())
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if result.Status == domain.EventStatusConsumed {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRoomEvent consumes committed room gift facts and projects revenue to the applicant room owner.
|
||||||
|
// 机器人礼物、非礼物事件和无扣费金额事件都跳过;房间 owner 从 room-service 当前事实解析,保证“开业流水”只统计申请人自己房间内发生的真实扣费。
|
||||||
|
func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.EventEnvelope) (int32, error) {
|
||||||
|
if s == 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
|
||||||
|
}
|
||||||
|
roomID := strings.TrimSpace(envelope.GetRoomId())
|
||||||
|
if gift.GetIsRobotGift() || roomID == "" || gift.GetCoinSpent() <= 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
eventCtx := appcode.WithContext(ctx, envelope.GetAppCode())
|
||||||
|
ownerUserID, err := s.fetchRoomOwner(eventCtx, roomID)
|
||||||
|
if err != nil || ownerUserID <= 0 {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return s.HandleGiftEvent(eventCtx, domain.GiftEvent{
|
||||||
|
EventID: strings.TrimSpace(envelope.GetEventId()),
|
||||||
|
RoomID: roomID,
|
||||||
|
RoomOwnerUserID: ownerUserID,
|
||||||
|
TargetUserID: gift.GetTargetUserId(),
|
||||||
|
CoinSpent: gift.GetCoinSpent(),
|
||||||
|
OccurredAtMS: envelope.GetOccurredAtMs(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ProcessSettlementBatch(ctx context.Context, runID string, workerID string, batchSize int32, lockTTL time.Duration) (claimed int32, processed int32, success int32, failure int32, hasMore bool, err error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return 0, 0, 0, 0, false, err
|
||||||
|
}
|
||||||
|
if batchSize <= 0 {
|
||||||
|
batchSize = 50
|
||||||
|
}
|
||||||
|
if lockTTL <= 0 {
|
||||||
|
lockTTL = 30 * time.Second
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(workerID) == "" {
|
||||||
|
workerID = "activity-agency-opening"
|
||||||
|
}
|
||||||
|
cycles, err := s.repository.ClaimDueAgencyOpeningCycles(ctx, workerID, s.now().UTC().UnixMilli(), lockTTL, batchSize)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, 0, false, err
|
||||||
|
}
|
||||||
|
for _, cycle := range cycles {
|
||||||
|
settlements, prepareErr := s.repository.PrepareAgencyOpeningSettlements(ctx, cycle, s.now().UTC().UnixMilli())
|
||||||
|
if prepareErr != nil {
|
||||||
|
logx.Error(ctx, "agency_opening_prepare_settlements_failed", prepareErr, slog.String("cycle_id", cycle.CycleID), slog.String("run_id", runID))
|
||||||
|
failure++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(settlements) == 0 {
|
||||||
|
if _, finishErr := s.repository.FinishAgencyOpeningCycleIfComplete(ctx, cycle.CycleID, s.now().UTC().UnixMilli()); finishErr != nil {
|
||||||
|
failure++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
success++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, settlement := range settlements {
|
||||||
|
processed++
|
||||||
|
if _, grantErr := s.grantSettlement(ctx, settlement); grantErr != nil {
|
||||||
|
logx.Error(ctx, "agency_opening_grant_failed", grantErr, slog.String("application_id", settlement.ApplicationID), slog.String("cycle_id", settlement.CycleID))
|
||||||
|
failure++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
success++
|
||||||
|
}
|
||||||
|
if _, finishErr := s.repository.FinishAgencyOpeningCycleIfComplete(ctx, cycle.CycleID, s.now().UTC().UnixMilli()); finishErr != nil {
|
||||||
|
failure++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return int32(len(cycles)), processed, success, failure, len(cycles) >= int(batchSize), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) grantSettlement(ctx context.Context, settlement domain.Application) (domain.Application, error) {
|
||||||
|
if settlement.Status == domain.ApplicationStatusGranted {
|
||||||
|
return settlement, nil
|
||||||
|
}
|
||||||
|
if settlement.RewardCoinAmount <= 0 {
|
||||||
|
nowMS := s.now().UTC().UnixMilli()
|
||||||
|
return s.repository.MarkAgencyOpeningApplicationGranted(ctx, settlement.ApplicationID, "", nowMS)
|
||||||
|
}
|
||||||
|
if s.wallet == nil {
|
||||||
|
err := xerr.New(xerr.Unavailable, "wallet client is not configured")
|
||||||
|
_ = s.repository.MarkAgencyOpeningApplicationFailed(ctx, settlement.ApplicationID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||||
|
return domain.Application{}, err
|
||||||
|
}
|
||||||
|
resp, err := s.wallet.CreditAgencyOpeningReward(ctx, &walletv1.CreditAgencyOpeningRewardRequest{
|
||||||
|
CommandId: settlement.WalletCommandID,
|
||||||
|
AppCode: appcode.FromContext(ctx),
|
||||||
|
TargetUserId: settlement.AgencyOwnerUserID,
|
||||||
|
Amount: settlement.RewardCoinAmount,
|
||||||
|
ApplicationId: settlement.ApplicationID,
|
||||||
|
CycleId: settlement.CycleID,
|
||||||
|
AgencyId: settlement.AgencyID,
|
||||||
|
RankNo: settlement.RewardRankNo,
|
||||||
|
ScoreCoins: settlement.ScoreCoinAmount,
|
||||||
|
Reason: rewardReason,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
_ = s.repository.MarkAgencyOpeningApplicationFailed(ctx, settlement.ApplicationID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||||
|
return domain.Application{}, err
|
||||||
|
}
|
||||||
|
return s.repository.MarkAgencyOpeningApplicationGranted(ctx, settlement.ApplicationID, resp.GetTransactionId(), resp.GetGrantedAtMs())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) upsertCycle(ctx context.Context, command domain.CycleCommand) (domain.Cycle, bool, error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return domain.Cycle{}, false, err
|
||||||
|
}
|
||||||
|
command.Cycle.Status = normalizeStatus(command.Cycle.Status)
|
||||||
|
if err := validateCycle(command.Cycle, command.OperatorAdminID); err != nil {
|
||||||
|
return domain.Cycle{}, false, err
|
||||||
|
}
|
||||||
|
return s.repository.UpsertAgencyOpeningCycle(ctx, command, s.now().UTC().UnixMilli())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) resolveOwnerAgency(ctx context.Context, userID int64) (domain.AgencySnapshot, bool, error) {
|
||||||
|
if s.agency == nil {
|
||||||
|
return domain.AgencySnapshot{}, false, xerr.New(xerr.Unavailable, "agency source is not configured")
|
||||||
|
}
|
||||||
|
return s.agency.ResolveOwnerAgency(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) fetchRoomOwner(ctx context.Context, roomID string) (int64, error) {
|
||||||
|
if s.room == nil {
|
||||||
|
return 0, xerr.New(xerr.Unavailable, "room query client is not configured")
|
||||||
|
}
|
||||||
|
roomID = strings.TrimSpace(roomID)
|
||||||
|
if roomID == "" {
|
||||||
|
return 0, xerr.New(xerr.InvalidArgument, "room_id is required")
|
||||||
|
}
|
||||||
|
resp, err := s.room.AdminGetRoom(ctx, &roomv1.AdminGetRoomRequest{
|
||||||
|
Meta: &roomv1.RequestMeta{
|
||||||
|
// 事件没有携带 owner 快照,按 room_id 生成稳定 request_id,方便追踪某次流水为何命中或跳过申请人。
|
||||||
|
RequestId: "agency-opening-room-owner:" + roomID,
|
||||||
|
AppCode: appcode.FromContext(ctx),
|
||||||
|
SentAtMs: s.now().UTC().UnixMilli(),
|
||||||
|
},
|
||||||
|
RoomId: roomID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return resp.GetRoom().GetOwnerUserId(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) requireRepository() error {
|
||||||
|
if s == nil || s.repository == nil {
|
||||||
|
return xerr.New(xerr.Unavailable, "agency opening repository is not configured")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCycle(cycle domain.Cycle, operatorAdminID int64) error {
|
||||||
|
if strings.TrimSpace(cycle.Title) == "" {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "title is required")
|
||||||
|
}
|
||||||
|
if !validStatus(cycle.Status) || cycle.Status == domain.StatusSettling || cycle.Status == domain.StatusSettled {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "status is invalid")
|
||||||
|
}
|
||||||
|
if cycle.StartMS <= 0 || cycle.EndMS <= cycle.StartMS {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "cycle period is invalid")
|
||||||
|
}
|
||||||
|
if cycle.MinHostCount <= 0 {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "min_host_count must be positive")
|
||||||
|
}
|
||||||
|
if cycle.MaxAgencyAgeDays < 0 {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "max_agency_age_days must not be negative")
|
||||||
|
}
|
||||||
|
if len(cycle.Rewards) == 0 {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "reward tiers are required")
|
||||||
|
}
|
||||||
|
seen := map[int32]bool{}
|
||||||
|
previousThreshold := int64(0)
|
||||||
|
for _, reward := range cycle.Rewards {
|
||||||
|
if reward.RankNo <= 0 || reward.ThresholdCoinSpent <= 0 || reward.RewardCoinAmount < 0 {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "reward tier threshold or amount is invalid")
|
||||||
|
}
|
||||||
|
if seen[reward.RankNo] {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "reward tier duplicated")
|
||||||
|
}
|
||||||
|
if previousThreshold > 0 && reward.ThresholdCoinSpent <= previousThreshold {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "reward tier threshold must increase")
|
||||||
|
}
|
||||||
|
seen[reward.RankNo] = true
|
||||||
|
previousThreshold = reward.ThresholdCoinSpent
|
||||||
|
}
|
||||||
|
if operatorAdminID <= 0 {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "operator_admin_id is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func eligible(cycle domain.Cycle, agency domain.AgencySnapshot, nowMS int64) (bool, string) {
|
||||||
|
if agency.Status != "active" {
|
||||||
|
return false, "active_agency_required"
|
||||||
|
}
|
||||||
|
if agency.HostCount <= cycle.MinHostCount {
|
||||||
|
return false, "host_count_not_enough"
|
||||||
|
}
|
||||||
|
if cycle.MaxAgencyAgeDays > 0 && agency.AgencyCreatedAtMS > 0 {
|
||||||
|
maxAgeMS := int64(cycle.MaxAgencyAgeDays) * int64(24*time.Hour/time.Millisecond)
|
||||||
|
if nowMS-agency.AgencyCreatedAtMS > maxAgeMS {
|
||||||
|
return false, "agency_too_old"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeStatus(value string) string {
|
||||||
|
value = strings.ToLower(strings.TrimSpace(value))
|
||||||
|
if value == "" {
|
||||||
|
return domain.StatusDraft
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeApplicationStatus(value string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func validStatus(value string) bool {
|
||||||
|
switch value {
|
||||||
|
case domain.StatusDraft, domain.StatusActive, domain.StatusDisabled, domain.StatusSettling, domain.StatusSettled:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,191 @@
|
|||||||
|
package agencyopening
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||||
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
domain "hyapp/services/activity-service/internal/domain/agencyopening"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEligibleRequiresHostCountGreaterThanConfigured(t *testing.T) {
|
||||||
|
cycle := domain.Cycle{MinHostCount: 10}
|
||||||
|
agency := domain.AgencySnapshot{Status: "active", HostCount: 10}
|
||||||
|
if ok, reason := eligible(cycle, agency, 1000); ok || reason != "host_count_not_enough" {
|
||||||
|
t.Fatalf("host_count equal to threshold must be rejected, got ok=%v reason=%q", ok, reason)
|
||||||
|
}
|
||||||
|
agency.HostCount = 11
|
||||||
|
if ok, reason := eligible(cycle, agency, 1000); !ok || reason != "" {
|
||||||
|
t.Fatalf("host_count above threshold must be eligible, got ok=%v reason=%q", ok, reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetStatusRejectsOwnerAlreadyAppliedInAnyCycle(t *testing.T) {
|
||||||
|
repo := &agencyOpeningFakeRepo{
|
||||||
|
currentCycle: domain.Cycle{CycleID: "current", Status: domain.StatusActive, StartMS: 1, EndMS: 10000, MinHostCount: 10},
|
||||||
|
hasCurrent: true,
|
||||||
|
application: domain.Application{
|
||||||
|
ApplicationID: "old-application",
|
||||||
|
CycleID: "previous",
|
||||||
|
AgencyOwnerUserID: 42,
|
||||||
|
Status: domain.ApplicationStatusApplied,
|
||||||
|
},
|
||||||
|
hasApplication: true,
|
||||||
|
}
|
||||||
|
svc := New(repo, nil, agencyOpeningFakeAgencySource{
|
||||||
|
snapshot: domain.AgencySnapshot{AgencyID: 7, OwnerUserID: 42, Status: "active", HostCount: 11},
|
||||||
|
ok: true,
|
||||||
|
}, nil)
|
||||||
|
svc.now = func() time.Time { return time.UnixMilli(5000).UTC() }
|
||||||
|
|
||||||
|
status, err := svc.GetStatus(appcode.WithContext(context.Background(), "lalu"), 42)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetStatus returned error: %v", err)
|
||||||
|
}
|
||||||
|
if !status.Joined || status.Eligible || status.IneligibleReason != "already_applied" {
|
||||||
|
t.Fatalf("expected already-applied owner to be blocked, got joined=%v eligible=%v reason=%q", status.Joined, status.Eligible, status.IneligibleReason)
|
||||||
|
}
|
||||||
|
if repo.ownerLookup != 42 {
|
||||||
|
t.Fatalf("expected repository lookup by owner_user_id, got %d", repo.ownerLookup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetStatusRequiresAgencyOwner(t *testing.T) {
|
||||||
|
repo := &agencyOpeningFakeRepo{
|
||||||
|
currentCycle: domain.Cycle{CycleID: "current", Status: domain.StatusActive, StartMS: 1, EndMS: 10000, MinHostCount: 10},
|
||||||
|
hasCurrent: true,
|
||||||
|
}
|
||||||
|
svc := New(repo, nil, agencyOpeningFakeAgencySource{ok: false}, nil)
|
||||||
|
svc.now = func() time.Time { return time.UnixMilli(5000).UTC() }
|
||||||
|
|
||||||
|
status, err := svc.GetStatus(appcode.WithContext(context.Background(), "lalu"), 42)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetStatus returned error: %v", err)
|
||||||
|
}
|
||||||
|
if status.Joined || status.Eligible || status.IneligibleReason != "active_agency_owner_required" {
|
||||||
|
t.Fatalf("expected non-agency user to be blocked, got joined=%v eligible=%v reason=%q", status.Joined, status.Eligible, status.IneligibleReason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleRoomEventScoresApplicantRoomOwner(t *testing.T) {
|
||||||
|
repo := &agencyOpeningFakeRepo{consumeStatus: domain.EventStatusConsumed}
|
||||||
|
room := &agencyOpeningFakeRoomClient{ownerUserID: 42}
|
||||||
|
svc := New(repo, nil, nil, room)
|
||||||
|
svc.now = func() time.Time { return time.UnixMilli(8000).UTC() }
|
||||||
|
body, err := proto.Marshal(&roomeventsv1.RoomGiftSent{
|
||||||
|
SenderUserId: 9,
|
||||||
|
TargetUserId: 88,
|
||||||
|
CoinSpent: 1200,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal gift: %v", err)
|
||||||
|
}
|
||||||
|
count, err := svc.HandleRoomEvent(context.Background(), &roomeventsv1.EventEnvelope{
|
||||||
|
EventId: "gift-event-1",
|
||||||
|
RoomId: "room-1",
|
||||||
|
EventType: "RoomGiftSent",
|
||||||
|
OccurredAtMs: 7000,
|
||||||
|
AppCode: "lalu",
|
||||||
|
Body: body,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("HandleRoomEvent returned error: %v", err)
|
||||||
|
}
|
||||||
|
if count != 1 {
|
||||||
|
t.Fatalf("expected one consumed event, got %d", count)
|
||||||
|
}
|
||||||
|
if room.roomID != "room-1" {
|
||||||
|
t.Fatalf("expected room lookup by event room_id, got %q", room.roomID)
|
||||||
|
}
|
||||||
|
if repo.consumed.RoomOwnerUserID != 42 || repo.consumed.TargetUserID != 88 || repo.consumed.CoinSpent != 1200 {
|
||||||
|
t.Fatalf("unexpected consumed event: %+v", repo.consumed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyOpeningFakeRepo struct {
|
||||||
|
currentCycle domain.Cycle
|
||||||
|
hasCurrent bool
|
||||||
|
application domain.Application
|
||||||
|
hasApplication bool
|
||||||
|
ownerLookup int64
|
||||||
|
consumeStatus string
|
||||||
|
consumed domain.GiftEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *agencyOpeningFakeRepo) ListAgencyOpeningCycles(context.Context, domain.ListQuery) ([]domain.Cycle, int64, error) {
|
||||||
|
return nil, 0, nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) GetAgencyOpeningCycle(context.Context, string) (domain.Cycle, error) {
|
||||||
|
return domain.Cycle{}, nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) UpsertAgencyOpeningCycle(context.Context, domain.CycleCommand, int64) (domain.Cycle, bool, error) {
|
||||||
|
return domain.Cycle{}, false, nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) SetAgencyOpeningCycleStatus(context.Context, string, string, int64, int64) (domain.Cycle, error) {
|
||||||
|
return domain.Cycle{}, nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) FindCurrentAgencyOpeningCycle(context.Context, int64) (domain.Cycle, bool, error) {
|
||||||
|
return r.currentCycle, r.hasCurrent, nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) GetAgencyOpeningApplicationByOwner(_ context.Context, ownerUserID int64) (domain.Application, bool, error) {
|
||||||
|
r.ownerLookup = ownerUserID
|
||||||
|
return r.application, r.hasApplication, nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) CreateAgencyOpeningApplication(context.Context, domain.Cycle, domain.AgencySnapshot, int64) (domain.Application, bool, error) {
|
||||||
|
return domain.Application{}, false, nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) ListAgencyOpeningApplications(context.Context, domain.ApplicationQuery) ([]domain.Application, int64, error) {
|
||||||
|
return nil, 0, nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) ConsumeAgencyOpeningGiftEvent(_ context.Context, event domain.GiftEvent, _ int64) (domain.EventResult, error) {
|
||||||
|
r.consumed = event
|
||||||
|
return domain.EventResult{EventID: event.EventID, Status: r.consumeStatus}, nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) ClaimDueAgencyOpeningCycles(context.Context, string, int64, time.Duration, int32) ([]domain.Cycle, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) PrepareAgencyOpeningSettlements(context.Context, domain.Cycle, int64) ([]domain.Application, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) MarkAgencyOpeningApplicationGranted(context.Context, string, string, int64) (domain.Application, error) {
|
||||||
|
return domain.Application{}, nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) MarkAgencyOpeningApplicationFailed(context.Context, string, string, int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (r *agencyOpeningFakeRepo) FinishAgencyOpeningCycleIfComplete(context.Context, string, int64) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyOpeningFakeAgencySource struct {
|
||||||
|
snapshot domain.AgencySnapshot
|
||||||
|
ok bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s agencyOpeningFakeAgencySource) ResolveOwnerAgency(context.Context, int64) (domain.AgencySnapshot, bool, error) {
|
||||||
|
return s.snapshot, s.ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyOpeningFakeRoomClient struct {
|
||||||
|
ownerUserID int64
|
||||||
|
roomID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *agencyOpeningFakeRoomClient) AdminGetRoom(_ context.Context, req *roomv1.AdminGetRoomRequest, _ ...grpc.CallOption) (*roomv1.AdminGetRoomResponse, error) {
|
||||||
|
c.roomID = req.GetRoomId()
|
||||||
|
return &roomv1.AdminGetRoomResponse{
|
||||||
|
Room: &roomv1.AdminRoomListItem{OwnerUserId: c.ownerUserID},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyOpeningFakeWalletClient struct{}
|
||||||
|
|
||||||
|
func (agencyOpeningFakeWalletClient) CreditAgencyOpeningReward(context.Context, *walletv1.CreditAgencyOpeningRewardRequest, ...grpc.CallOption) (*walletv1.CreditAgencyOpeningRewardResponse, error) {
|
||||||
|
return &walletv1.CreditAgencyOpeningRewardResponse{}, nil
|
||||||
|
}
|
||||||
@ -0,0 +1,297 @@
|
|||||||
|
package cpweeklyrank
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/logx"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
domain "hyapp/services/activity-service/internal/domain/cpweeklyrank"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cpWeeklyRankRewardReason = "cp weekly rank reward"
|
||||||
|
|
||||||
|
// Repository is the activity-service storage boundary for CP weekly rank config and settlement facts.
|
||||||
|
type Repository interface {
|
||||||
|
GetCPWeeklyRankConfig(ctx context.Context) (domain.Config, bool, error)
|
||||||
|
UpdateCPWeeklyRankConfig(ctx context.Context, config domain.Config, nowMS int64) (domain.Config, error)
|
||||||
|
ListCPWeeklyRankSettlements(ctx context.Context, periodStartMS int64, periodEndMS int64) ([]domain.Settlement, error)
|
||||||
|
PrepareCPWeeklyRankSettlements(ctx context.Context, config domain.Config, entries []domain.Entry, periodStartMS int64, periodEndMS int64, nowMS int64) ([]domain.Settlement, error)
|
||||||
|
ListPendingCPWeeklyRankSettlements(ctx context.Context, periodStartMS int64, periodEndMS int64, limit int) ([]domain.Settlement, error)
|
||||||
|
MarkCPWeeklyRankSettlementGranted(ctx context.Context, settlementID string, walletGrantID string, nowMS int64) error
|
||||||
|
MarkCPWeeklyRankSettlementFailed(ctx context.Context, settlementID string, reason string, nowMS int64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RankSource reads user-service owned CP weekly score snapshots; activity-service never scans user DB directly.
|
||||||
|
type RankSource interface {
|
||||||
|
ListCPWeeklyRankEntries(ctx context.Context, relationType string, startMS int64, endMS int64, limit int32) ([]domain.Entry, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WalletClient owns the external resource-group grant side effect.
|
||||||
|
type WalletClient interface {
|
||||||
|
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest, opts ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service owns CP weekly rank config, H5 reads and weekly reward settlement.
|
||||||
|
type Service struct {
|
||||||
|
repository Repository
|
||||||
|
rankSource RankSource
|
||||||
|
wallet WalletClient
|
||||||
|
now func() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(repository Repository, rankSource RankSource, wallet WalletClient) *Service {
|
||||||
|
return &Service{repository: repository, rankSource: rankSource, wallet: wallet, now: time.Now}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetClock(now func() time.Time) {
|
||||||
|
if now != nil {
|
||||||
|
s.now = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetConfig(ctx context.Context) (domain.Config, error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return domain.Config{}, err
|
||||||
|
}
|
||||||
|
config, exists, err := s.repository.GetCPWeeklyRankConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Config{}, err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return defaultConfig(ctx), nil
|
||||||
|
}
|
||||||
|
return normalizeConfigForRead(config), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateConfig(ctx context.Context, config domain.Config, operatorAdminID int64) (domain.Config, error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return domain.Config{}, err
|
||||||
|
}
|
||||||
|
if operatorAdminID <= 0 {
|
||||||
|
return domain.Config{}, xerr.New(xerr.InvalidArgument, "operator_admin_id is required")
|
||||||
|
}
|
||||||
|
config.AppCode = appcode.FromContext(ctx)
|
||||||
|
config.ActivityCode = domain.ActivityCode
|
||||||
|
config.RelationType = "cp"
|
||||||
|
config.UpdatedByAdminID = operatorAdminID
|
||||||
|
config = normalizeConfigForWrite(config)
|
||||||
|
if err := validateConfig(config); err != nil {
|
||||||
|
return domain.Config{}, err
|
||||||
|
}
|
||||||
|
return s.repository.UpdateCPWeeklyRankConfig(ctx, config, s.now().UTC().UnixMilli())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetStatus(ctx context.Context, limit int32, nowMS int64) (domain.Status, error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return domain.Status{}, err
|
||||||
|
}
|
||||||
|
if s.rankSource == nil {
|
||||||
|
return domain.Status{}, xerr.New(xerr.Unavailable, "cp weekly rank source is not configured")
|
||||||
|
}
|
||||||
|
now := s.now().UTC()
|
||||||
|
if nowMS > 0 {
|
||||||
|
now = time.UnixMilli(nowMS).UTC()
|
||||||
|
}
|
||||||
|
start, end := WeekWindow(now)
|
||||||
|
config, err := s.GetConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Status{}, err
|
||||||
|
}
|
||||||
|
result := domain.Status{
|
||||||
|
Config: config,
|
||||||
|
PeriodStartMS: start.UnixMilli(),
|
||||||
|
PeriodEndMS: end.UnixMilli(),
|
||||||
|
ServerTimeMS: now.UnixMilli(),
|
||||||
|
}
|
||||||
|
if !config.Enabled {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
if limit <= 0 || limit > config.TopCount {
|
||||||
|
limit = config.TopCount
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 3
|
||||||
|
}
|
||||||
|
entries, err := s.rankSource.ListCPWeeklyRankEntries(ctx, config.RelationType, result.PeriodStartMS, result.PeriodEndMS, limit)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Status{}, err
|
||||||
|
}
|
||||||
|
result.Entries = entries
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListSettlements(ctx context.Context, periodStartMS int64, periodEndMS int64) ([]domain.Settlement, error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.repository.ListCPWeeklyRankSettlements(ctx, periodStartMS, periodEndMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessSettlementBatch freezes the previous complete UTC week and grants each Top relationship reward to both users.
|
||||||
|
func (s *Service) ProcessSettlementBatch(ctx context.Context, runID string, workerID string, batchSize int32) (claimed int32, processed int32, success int32, failure int32, hasMore bool, err error) {
|
||||||
|
if err := s.requireRepository(); err != nil {
|
||||||
|
return 0, 0, 0, 0, false, err
|
||||||
|
}
|
||||||
|
if s.rankSource == nil {
|
||||||
|
return 0, 0, 0, 0, false, xerr.New(xerr.Unavailable, "cp weekly rank source is not configured")
|
||||||
|
}
|
||||||
|
if s.wallet == nil {
|
||||||
|
return 0, 0, 0, 0, false, xerr.New(xerr.Unavailable, "wallet client is not configured")
|
||||||
|
}
|
||||||
|
if batchSize <= 0 {
|
||||||
|
batchSize = 50
|
||||||
|
}
|
||||||
|
config, err := s.GetConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, 0, false, err
|
||||||
|
}
|
||||||
|
if !config.Enabled || len(config.Rewards) == 0 {
|
||||||
|
return 0, 0, 0, 0, false, nil
|
||||||
|
}
|
||||||
|
periodStart, periodEnd := PreviousWeekWindow(s.now().UTC())
|
||||||
|
entries, err := s.rankSource.ListCPWeeklyRankEntries(ctx, config.RelationType, periodStart.UnixMilli(), periodEnd.UnixMilli(), config.TopCount)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, 0, false, err
|
||||||
|
}
|
||||||
|
settlements, err := s.repository.PrepareCPWeeklyRankSettlements(ctx, config, entries, periodStart.UnixMilli(), periodEnd.UnixMilli(), s.now().UTC().UnixMilli())
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, 0, false, err
|
||||||
|
}
|
||||||
|
claimed = int32(len(settlements))
|
||||||
|
pending, err := s.repository.ListPendingCPWeeklyRankSettlements(ctx, periodStart.UnixMilli(), periodEnd.UnixMilli(), int(batchSize))
|
||||||
|
if err != nil {
|
||||||
|
return claimed, 0, 0, 0, false, err
|
||||||
|
}
|
||||||
|
hasMore = len(pending) >= int(batchSize)
|
||||||
|
for _, settlement := range pending {
|
||||||
|
processed++
|
||||||
|
// activity-service 只决定 CP 周榜获奖关系和资源组,wallet-service 负责展开资源组和写 grant 流水。
|
||||||
|
// 失败时保持同一个 wallet_command_id 回到 failed,下一轮 cron 继续重试,防止网络抖动造成重复发奖。
|
||||||
|
resp, grantErr := s.wallet.GrantResourceGroup(ctx, &walletv1.GrantResourceGroupRequest{
|
||||||
|
CommandId: settlement.WalletCommandID,
|
||||||
|
AppCode: appcode.FromContext(ctx),
|
||||||
|
TargetUserId: settlement.UserID,
|
||||||
|
GroupId: settlement.ResourceGroupID,
|
||||||
|
Reason: cpWeeklyRankRewardReason,
|
||||||
|
OperatorUserId: settlement.UserID,
|
||||||
|
GrantSource: domain.GrantSourceCPWeeklyRank,
|
||||||
|
})
|
||||||
|
if grantErr != nil {
|
||||||
|
logx.Error(ctx, "cp_weekly_rank_grant_resource_group_failed", grantErr, slog.String("settlement_id", settlement.SettlementID), slog.String("run_id", runID), slog.String("worker_id", workerID))
|
||||||
|
failure++
|
||||||
|
_ = s.repository.MarkCPWeeklyRankSettlementFailed(ctx, settlement.SettlementID, xerr.MessageOf(grantErr), s.now().UTC().UnixMilli())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
walletGrantID := ""
|
||||||
|
if resp.GetGrant() != nil {
|
||||||
|
walletGrantID = resp.GetGrant().GetGrantId()
|
||||||
|
}
|
||||||
|
if err := s.repository.MarkCPWeeklyRankSettlementGranted(ctx, settlement.SettlementID, walletGrantID, s.now().UTC().UnixMilli()); err != nil {
|
||||||
|
logx.Error(ctx, "cp_weekly_rank_mark_granted_failed", err, slog.String("settlement_id", settlement.SettlementID), slog.String("wallet_grant_id", walletGrantID))
|
||||||
|
failure++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
success++
|
||||||
|
}
|
||||||
|
return claimed, processed, success, failure, hasMore, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) requireRepository() error {
|
||||||
|
if s == nil || s.repository == nil {
|
||||||
|
return xerr.New(xerr.Unavailable, "cp weekly rank repository is not configured")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfig(ctx context.Context) domain.Config {
|
||||||
|
return domain.Config{
|
||||||
|
AppCode: appcode.FromContext(ctx),
|
||||||
|
ActivityCode: domain.ActivityCode,
|
||||||
|
Enabled: true,
|
||||||
|
RelationType: "cp",
|
||||||
|
TopCount: 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeConfigForRead(config domain.Config) domain.Config {
|
||||||
|
config.ActivityCode = domain.ActivityCode
|
||||||
|
config.RelationType = "cp"
|
||||||
|
if config.TopCount <= 0 {
|
||||||
|
config.TopCount = 3
|
||||||
|
}
|
||||||
|
config.Rewards = normalizeRewards(config.Rewards, config.TopCount)
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeConfigForWrite(config domain.Config) domain.Config {
|
||||||
|
if config.TopCount <= 0 {
|
||||||
|
config.TopCount = 3
|
||||||
|
}
|
||||||
|
if config.TopCount > 20 {
|
||||||
|
config.TopCount = 20
|
||||||
|
}
|
||||||
|
config.Rewards = normalizeRewards(config.Rewards, config.TopCount)
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeRewards(rewards []domain.Reward, topCount int32) []domain.Reward {
|
||||||
|
seen := make(map[int32]domain.Reward, len(rewards))
|
||||||
|
for _, reward := range rewards {
|
||||||
|
if reward.RankNo <= 0 || reward.RankNo > topCount || reward.ResourceGroupID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
reward.RankNo = int32(reward.RankNo)
|
||||||
|
seen[reward.RankNo] = reward
|
||||||
|
}
|
||||||
|
normalized := make([]domain.Reward, 0, len(seen))
|
||||||
|
for _, reward := range seen {
|
||||||
|
normalized = append(normalized, reward)
|
||||||
|
}
|
||||||
|
sort.Slice(normalized, func(i, j int) bool { return normalized[i].RankNo < normalized[j].RankNo })
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConfig(config domain.Config) error {
|
||||||
|
if config.Enabled && len(config.Rewards) == 0 {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "cp weekly rank rewards are required when enabled")
|
||||||
|
}
|
||||||
|
for _, reward := range config.Rewards {
|
||||||
|
if reward.RankNo <= 0 || reward.RankNo > config.TopCount {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "cp weekly rank reward rank_no is invalid")
|
||||||
|
}
|
||||||
|
if reward.ResourceGroupID <= 0 {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "cp weekly rank reward resource_group_id is required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WeekWindow returns the current UTC week [Monday 00:00, next Monday 00:00).
|
||||||
|
func WeekWindow(now time.Time) (time.Time, time.Time) {
|
||||||
|
dayStart := time.Date(now.UTC().Year(), now.UTC().Month(), now.UTC().Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
weekday := int(dayStart.Weekday())
|
||||||
|
if weekday == 0 {
|
||||||
|
weekday = 7
|
||||||
|
}
|
||||||
|
start := dayStart.AddDate(0, 0, 1-weekday)
|
||||||
|
return start, start.AddDate(0, 0, 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreviousWeekWindow returns the complete UTC week immediately before now.
|
||||||
|
func PreviousWeekWindow(now time.Time) (time.Time, time.Time) {
|
||||||
|
currentStart, _ := WeekWindow(now)
|
||||||
|
previousStart := currentStart.AddDate(0, 0, -7)
|
||||||
|
return previousStart, currentStart
|
||||||
|
}
|
||||||
|
|
||||||
|
func WalletCommandID(periodStartMS int64, rankNo int32, relationshipID string, userID int64) string {
|
||||||
|
return fmt.Sprintf("cp_weekly_rank:%d:%d:%s:%d", periodStartMS, rankNo, strings.TrimSpace(relationshipID), userID)
|
||||||
|
}
|
||||||
265
services/activity-service/internal/service/message/notice.go
Normal file
265
services/activity-service/internal/service/message/notice.go
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
messagedomain "hyapp/services/activity-service/internal/domain/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultNoticeProducer = "activity-service"
|
||||||
|
|
||||||
|
// NoticeCommand is the common producer-facing command for one user-visible system/activity message.
|
||||||
|
type NoticeCommand struct {
|
||||||
|
TargetUserID int64
|
||||||
|
Producer string
|
||||||
|
ProducerEventID string
|
||||||
|
ProducerEventType string
|
||||||
|
AggregateType string
|
||||||
|
AggregateID string
|
||||||
|
TemplateID string
|
||||||
|
TemplateVersion string
|
||||||
|
Title string
|
||||||
|
Summary string
|
||||||
|
Body string
|
||||||
|
IconURL string
|
||||||
|
ImageURL string
|
||||||
|
ActionType string
|
||||||
|
ActionParam string
|
||||||
|
Priority int32
|
||||||
|
SentAtMS int64
|
||||||
|
ExpireAtMS int64
|
||||||
|
Metadata map[string]any
|
||||||
|
MetadataJSON string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoticeResult keeps the inbox row and the idempotent create flag together.
|
||||||
|
type NoticeResult struct {
|
||||||
|
Message messagedomain.InboxMessage
|
||||||
|
Created bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoticeFanoutCommand is the common command for background system/activity broadcasts.
|
||||||
|
type NoticeFanoutCommand struct {
|
||||||
|
CommandID string
|
||||||
|
MessageType string
|
||||||
|
TargetScope string
|
||||||
|
TargetUserID int64
|
||||||
|
UserIDs []int64
|
||||||
|
RegionID int64
|
||||||
|
Country string
|
||||||
|
ProducerEventType string
|
||||||
|
AggregateType string
|
||||||
|
AggregateID string
|
||||||
|
TemplateID string
|
||||||
|
TemplateVersion string
|
||||||
|
Title string
|
||||||
|
Summary string
|
||||||
|
Body string
|
||||||
|
IconURL string
|
||||||
|
ImageURL string
|
||||||
|
ActionType string
|
||||||
|
ActionParam string
|
||||||
|
Priority int32
|
||||||
|
SentAtMS int64
|
||||||
|
ExpireAtMS int64
|
||||||
|
Metadata map[string]any
|
||||||
|
MetadataJSON string
|
||||||
|
BatchSize int32
|
||||||
|
CreatedBy string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSystemNotice is the small public helper for account, wallet, room and application messages.
|
||||||
|
func (s *Service) CreateSystemNotice(ctx context.Context, cmd NoticeCommand) (NoticeResult, error) {
|
||||||
|
return s.CreateNotice(ctx, messagedomain.SectionSystem, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateActivityNotice is the small public helper for activity, reward and campaign messages.
|
||||||
|
func (s *Service) CreateActivityNotice(ctx context.Context, cmd NoticeCommand) (NoticeResult, error) {
|
||||||
|
return s.CreateNotice(ctx, messagedomain.SectionActivity, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNotice normalizes producer defaults and metadata before delegating to the inbox owner.
|
||||||
|
func (s *Service) CreateNotice(ctx context.Context, messageType string, cmd NoticeCommand) (NoticeResult, error) {
|
||||||
|
metadataJSON, err := noticeMetadataJSON(cmd.Metadata, cmd.MetadataJSON)
|
||||||
|
if err != nil {
|
||||||
|
return NoticeResult{}, err
|
||||||
|
}
|
||||||
|
// The existing CreateInboxMessage method is still the only place that writes user_inbox_messages.
|
||||||
|
// This helper only narrows the public call shape so producers use the same idempotency and content rules.
|
||||||
|
message, created, err := s.CreateInboxMessage(ctx, messagedomain.CreateInput{
|
||||||
|
TargetUserID: cmd.TargetUserID,
|
||||||
|
Producer: firstNoticeText(cmd.Producer, defaultNoticeProducer),
|
||||||
|
ProducerEventID: cmd.ProducerEventID,
|
||||||
|
ProducerEventType: cmd.ProducerEventType,
|
||||||
|
MessageType: messageType,
|
||||||
|
AggregateType: cmd.AggregateType,
|
||||||
|
AggregateID: cmd.AggregateID,
|
||||||
|
TemplateID: cmd.TemplateID,
|
||||||
|
TemplateVersion: cmd.TemplateVersion,
|
||||||
|
Title: cmd.Title,
|
||||||
|
Summary: cmd.Summary,
|
||||||
|
Body: cmd.Body,
|
||||||
|
IconURL: cmd.IconURL,
|
||||||
|
ImageURL: cmd.ImageURL,
|
||||||
|
ActionType: cmd.ActionType,
|
||||||
|
ActionParam: cmd.ActionParam,
|
||||||
|
Priority: cmd.Priority,
|
||||||
|
SentAtMS: cmd.SentAtMS,
|
||||||
|
ExpireAtMS: cmd.ExpireAtMS,
|
||||||
|
MetadataJSON: metadataJSON,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return NoticeResult{}, err
|
||||||
|
}
|
||||||
|
return NoticeResult{Message: message, Created: created}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNoticeFanout records a broadcast command for the fanout worker instead of looping in the caller.
|
||||||
|
func (s *Service) CreateNoticeFanout(ctx context.Context, cmd NoticeFanoutCommand) (messagedomain.FanoutJob, bool, error) {
|
||||||
|
messageType := normalizeSection(cmd.MessageType)
|
||||||
|
if messageType == "" {
|
||||||
|
messageType = messagedomain.SectionSystem
|
||||||
|
}
|
||||||
|
targetFilterJSON, err := noticeTargetFilterJSON(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return messagedomain.FanoutJob{}, false, err
|
||||||
|
}
|
||||||
|
templateSnapshotJSON, err := noticeTemplateSnapshotJSON(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return messagedomain.FanoutJob{}, false, err
|
||||||
|
}
|
||||||
|
// command_id is the broadcast idempotency key. Retrying the same admin/activity command returns the
|
||||||
|
// existing job; changing payload under the same command_id is rejected by CreateFanoutJob.
|
||||||
|
return s.CreateFanoutJob(ctx, messagedomain.CreateFanoutInput{
|
||||||
|
CommandID: strings.TrimSpace(cmd.CommandID),
|
||||||
|
MessageType: messageType,
|
||||||
|
TargetScope: strings.TrimSpace(cmd.TargetScope),
|
||||||
|
TargetFilterJSON: targetFilterJSON,
|
||||||
|
TemplateSnapshotJSON: templateSnapshotJSON,
|
||||||
|
BatchSize: cmd.BatchSize,
|
||||||
|
CreatedBy: strings.TrimSpace(cmd.CreatedBy),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func noticeTargetFilterJSON(cmd NoticeFanoutCommand) (string, error) {
|
||||||
|
scope := strings.TrimSpace(cmd.TargetScope)
|
||||||
|
var payload map[string]any
|
||||||
|
switch scope {
|
||||||
|
case messagedomain.TargetScopeSingleUser:
|
||||||
|
if cmd.TargetUserID <= 0 {
|
||||||
|
return "", xerr.New(xerr.InvalidArgument, "target_user_id is required")
|
||||||
|
}
|
||||||
|
payload = map[string]any{"target_user_id": cmd.TargetUserID}
|
||||||
|
case messagedomain.TargetScopeUserIDs:
|
||||||
|
userIDs := positiveUniqueInt64s(cmd.UserIDs)
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return "", xerr.New(xerr.InvalidArgument, "user_ids is required")
|
||||||
|
}
|
||||||
|
payload = map[string]any{"user_ids": userIDs}
|
||||||
|
case messagedomain.TargetScopeRegion:
|
||||||
|
if cmd.RegionID <= 0 {
|
||||||
|
return "", xerr.New(xerr.InvalidArgument, "region_id is required")
|
||||||
|
}
|
||||||
|
payload = map[string]any{"region_id": cmd.RegionID}
|
||||||
|
case messagedomain.TargetScopeCountry:
|
||||||
|
country := strings.TrimSpace(cmd.Country)
|
||||||
|
if country == "" {
|
||||||
|
return "", xerr.New(xerr.InvalidArgument, "country is required")
|
||||||
|
}
|
||||||
|
payload = map[string]any{"country": country}
|
||||||
|
case messagedomain.TargetScopeAllActiveUsers:
|
||||||
|
payload = map[string]any{}
|
||||||
|
default:
|
||||||
|
return "", xerr.New(xerr.InvalidArgument, "target_scope is invalid")
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(encoded), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func noticeTemplateSnapshotJSON(cmd NoticeFanoutCommand) (string, error) {
|
||||||
|
metadataJSON, err := noticeMetadataJSON(cmd.Metadata, cmd.MetadataJSON)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
payload := map[string]any{
|
||||||
|
"producer_event_type": strings.TrimSpace(cmd.ProducerEventType),
|
||||||
|
"aggregate_type": strings.TrimSpace(cmd.AggregateType),
|
||||||
|
"aggregate_id": strings.TrimSpace(cmd.AggregateID),
|
||||||
|
"template_id": strings.TrimSpace(cmd.TemplateID),
|
||||||
|
"template_version": strings.TrimSpace(cmd.TemplateVersion),
|
||||||
|
"title": strings.TrimSpace(cmd.Title),
|
||||||
|
"summary": strings.TrimSpace(cmd.Summary),
|
||||||
|
"body": strings.TrimSpace(cmd.Body),
|
||||||
|
"icon_url": strings.TrimSpace(cmd.IconURL),
|
||||||
|
"image_url": strings.TrimSpace(cmd.ImageURL),
|
||||||
|
"action_type": strings.TrimSpace(cmd.ActionType),
|
||||||
|
"action_param": strings.TrimSpace(cmd.ActionParam),
|
||||||
|
"priority": cmd.Priority,
|
||||||
|
"sent_at_ms": cmd.SentAtMS,
|
||||||
|
"expire_at_ms": cmd.ExpireAtMS,
|
||||||
|
}
|
||||||
|
if metadataJSON != "" {
|
||||||
|
var parsed any
|
||||||
|
if err := json.Unmarshal([]byte(metadataJSON), &parsed); err != nil {
|
||||||
|
return "", xerr.New(xerr.InvalidArgument, "metadata_json is invalid")
|
||||||
|
}
|
||||||
|
payload["metadata_json"] = parsed
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(encoded), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func noticeMetadataJSON(metadata map[string]any, metadataJSON string) (string, error) {
|
||||||
|
metadataJSON = strings.TrimSpace(metadataJSON)
|
||||||
|
if len(metadata) > 0 && metadataJSON != "" {
|
||||||
|
return "", xerr.New(xerr.InvalidArgument, "metadata and metadata_json cannot both be set")
|
||||||
|
}
|
||||||
|
if metadataJSON != "" {
|
||||||
|
if !json.Valid([]byte(metadataJSON)) {
|
||||||
|
return "", xerr.New(xerr.InvalidArgument, "metadata_json is invalid")
|
||||||
|
}
|
||||||
|
return metadataJSON, nil
|
||||||
|
}
|
||||||
|
if len(metadata) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(encoded), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func positiveUniqueInt64s(values []int64) []int64 {
|
||||||
|
seen := make(map[int64]struct{}, len(values))
|
||||||
|
out := make([]int64, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
if value <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[value]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
out = append(out, value)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNoticeText(values ...string) string {
|
||||||
|
for _, value := range values {
|
||||||
|
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@ -178,6 +178,72 @@ func TestCreateFanoutJobIsIdempotentAndDetectsCommandConflict(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNoticeHelpersCreateInboxAndFanoutPayloads(t *testing.T) {
|
||||||
|
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
svc := messageservice.New(messageservice.Config{NodeID: "activity-a"}, repository)
|
||||||
|
svc.SetClock(func() time.Time { return time.UnixMilli(1_800_000_025_000) })
|
||||||
|
|
||||||
|
notice, err := svc.CreateActivityNotice(ctx, messageservice.NoticeCommand{
|
||||||
|
TargetUserID: 42,
|
||||||
|
ProducerEventID: "activity-reward-42-1",
|
||||||
|
ProducerEventType: "activity_reward",
|
||||||
|
AggregateType: "activity",
|
||||||
|
AggregateID: "act_01",
|
||||||
|
Title: "活动奖励到账",
|
||||||
|
Summary: "你的活动奖励已发放。",
|
||||||
|
ActionType: "activity_detail",
|
||||||
|
ActionParam: "activity_id=act_01",
|
||||||
|
Metadata: map[string]any{"activity_id": "act_01"},
|
||||||
|
})
|
||||||
|
if err != nil || !notice.Created || notice.Message.MessageType != messagedomain.SectionActivity || notice.Message.Producer != "activity-service" {
|
||||||
|
t.Fatalf("CreateActivityNotice failed: notice=%+v err=%v", notice, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
job, created, err := svc.CreateNoticeFanout(ctx, messageservice.NoticeFanoutCommand{
|
||||||
|
CommandID: "admin-full-server-notice-1",
|
||||||
|
MessageType: messagedomain.SectionSystem,
|
||||||
|
TargetScope: messagedomain.TargetScopeUserIDs,
|
||||||
|
UserIDs: []int64{42, 43, 42, 0},
|
||||||
|
ProducerEventType: "admin_full_server_notice",
|
||||||
|
AggregateType: "admin_notice",
|
||||||
|
AggregateID: "notice_1",
|
||||||
|
Title: "全服维护通知",
|
||||||
|
Summary: "今晚 23:00 进行短暂维护。",
|
||||||
|
Body: "维护期间部分功能可能短暂不可用。",
|
||||||
|
ActionType: "app_h5",
|
||||||
|
ActionParam: "https://example.com/notice",
|
||||||
|
MetadataJSON: `{"source":"admin"}`,
|
||||||
|
BatchSize: 500,
|
||||||
|
CreatedBy: "admin:1",
|
||||||
|
})
|
||||||
|
if err != nil || !created || job.MessageType != messagedomain.SectionSystem || job.TargetScope != messagedomain.TargetScopeUserIDs {
|
||||||
|
t.Fatalf("CreateNoticeFanout failed: created=%v job=%+v err=%v", created, job, err)
|
||||||
|
}
|
||||||
|
if job.TargetFilterJSON != `{"user_ids":[42,43]}` {
|
||||||
|
t.Fatalf("fanout target filter should deduplicate user_ids, got %s", job.TargetFilterJSON)
|
||||||
|
}
|
||||||
|
if _, created, err := svc.CreateNoticeFanout(ctx, messageservice.NoticeFanoutCommand{
|
||||||
|
CommandID: "admin-full-server-notice-1",
|
||||||
|
MessageType: messagedomain.SectionSystem,
|
||||||
|
TargetScope: messagedomain.TargetScopeUserIDs,
|
||||||
|
UserIDs: []int64{42, 43, 42, 0},
|
||||||
|
ProducerEventType: "admin_full_server_notice",
|
||||||
|
AggregateType: "admin_notice",
|
||||||
|
AggregateID: "notice_1",
|
||||||
|
Title: "全服维护通知",
|
||||||
|
Summary: "今晚 23:00 进行短暂维护。",
|
||||||
|
Body: "维护期间部分功能可能短暂不可用。",
|
||||||
|
ActionType: "app_h5",
|
||||||
|
ActionParam: "https://example.com/notice",
|
||||||
|
MetadataJSON: `{"source":"admin"}`,
|
||||||
|
BatchSize: 500,
|
||||||
|
CreatedBy: "admin:1",
|
||||||
|
}); err != nil || created {
|
||||||
|
t.Fatalf("notice fanout duplicate should be idempotent: created=%v err=%v", created, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFanoutWorkerMaterializesUserIDTargetsByCursor(t *testing.T) {
|
func TestFanoutWorkerMaterializesUserIDTargetsByCursor(t *testing.T) {
|
||||||
ctx := appcode.WithContext(context.Background(), "lalu")
|
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
|
|||||||
@ -0,0 +1,142 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
func (r *Repository) ensureAgencyOpeningTables(ctx context.Context) error {
|
||||||
|
statements := []string{
|
||||||
|
`CREATE TABLE IF NOT EXISTS agency_opening_cycles (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||||
|
activity_code VARCHAR(64) NOT NULL DEFAULT 'agency_opening' COMMENT '活动编码',
|
||||||
|
title VARCHAR(128) NOT NULL DEFAULT '' COMMENT '周期标题',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/active/disabled/settling/settled',
|
||||||
|
start_ms BIGINT NOT NULL COMMENT 'UTC epoch ms 开始,包含',
|
||||||
|
end_ms BIGINT NOT NULL COMMENT 'UTC epoch ms 结束,不包含',
|
||||||
|
min_host_count INT NOT NULL DEFAULT 10 COMMENT '申请要求的有效主播数',
|
||||||
|
max_agency_age_days INT NOT NULL DEFAULT 7 COMMENT '代理创建后允许申请的最大天数,0 表示不限制',
|
||||||
|
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建管理员',
|
||||||
|
updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新管理员',
|
||||||
|
settled_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '结算完成时间',
|
||||||
|
locked_by VARCHAR(96) NOT NULL DEFAULT '' COMMENT '结算 worker 锁',
|
||||||
|
lock_until_ms 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',
|
||||||
|
PRIMARY KEY (app_code, cycle_id),
|
||||||
|
KEY idx_agency_opening_cycle_current (app_code, activity_code, status, start_ms, end_ms),
|
||||||
|
KEY idx_agency_opening_cycle_due (app_code, activity_code, status, end_ms, lock_until_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业活动周期'`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS agency_opening_cycle_rewards (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||||
|
rank_no INT NOT NULL COMMENT '档位序号',
|
||||||
|
threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '命中该档需要的代理房间流水金币',
|
||||||
|
reward_coin_amount 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',
|
||||||
|
PRIMARY KEY (app_code, cycle_id, rank_no)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业流水奖励档位'`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS agency_opening_applications (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
application_id VARCHAR(96) NOT NULL COMMENT '申请 ID',
|
||||||
|
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||||
|
agency_id BIGINT NOT NULL COMMENT '代理 ID',
|
||||||
|
agency_owner_user_id BIGINT NOT NULL COMMENT '代理 owner 用户 ID',
|
||||||
|
agency_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '代理名称快照',
|
||||||
|
host_count INT NOT NULL DEFAULT 0 COMMENT '申请时有效主播数快照',
|
||||||
|
agency_created_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '代理创建时间快照',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'applied' COMMENT 'applied/pending/granted/failed',
|
||||||
|
score_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '周期内开业流水金币',
|
||||||
|
reward_rank_no INT NOT NULL DEFAULT 0 COMMENT '结算命中的档位序号',
|
||||||
|
reward_threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '结算命中的流水阈值',
|
||||||
|
reward_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '结算奖励金币',
|
||||||
|
wallet_command_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '钱包幂等命令',
|
||||||
|
wallet_transaction_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
|
||||||
|
failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||||
|
applied_at_ms BIGINT NOT NULL COMMENT '申请时间',
|
||||||
|
granted_at_ms 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',
|
||||||
|
PRIMARY KEY (app_code, application_id),
|
||||||
|
UNIQUE KEY uk_agency_opening_owner_once (app_code, agency_owner_user_id),
|
||||||
|
KEY idx_agency_opening_wallet_command (app_code, wallet_command_id),
|
||||||
|
KEY idx_agency_opening_application_owner (app_code, agency_owner_user_id, applied_at_ms),
|
||||||
|
KEY idx_agency_opening_agency_cycle (app_code, cycle_id, agency_id),
|
||||||
|
KEY idx_agency_opening_application_rank (app_code, cycle_id, score_coin_amount, applied_at_ms, agency_id),
|
||||||
|
KEY idx_agency_opening_application_status (app_code, cycle_id, status, updated_at_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理开业申请和结算事实'`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS agency_opening_score_events (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
source_event_id VARCHAR(128) NOT NULL COMMENT 'room-service outbox event_id',
|
||||||
|
cycle_id VARCHAR(96) NOT NULL COMMENT '代理开业周期 ID',
|
||||||
|
application_id VARCHAR(96) NOT NULL COMMENT '命中的申请 ID',
|
||||||
|
agency_id BIGINT NOT NULL COMMENT '代理 ID',
|
||||||
|
target_user_id BIGINT NOT NULL COMMENT '收礼主播用户 ID',
|
||||||
|
score_delta BIGINT NOT NULL COMMENT '本次流水金币',
|
||||||
|
occurred_at_ms BIGINT NOT NULL COMMENT '事件发生时间,UTC epoch ms',
|
||||||
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (app_code, source_event_id),
|
||||||
|
KEY idx_agency_opening_score_event_application (app_code, application_id, occurred_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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := r.ensureAgencyOpeningRewardTierColumns(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.ensureAgencyOpeningApplicationOwnerOnceIndex(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ensureAgencyOpeningRewardTierColumns(ctx context.Context) error {
|
||||||
|
if exists, err := r.columnExists(ctx, "agency_opening_cycle_rewards", "threshold_coin_spent"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
// 旧本地表按 Top 名次发奖;新增阈值列后,rank_no 只作为档位序号保留,实际结算按 threshold_coin_spent 命中。
|
||||||
|
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_cycle_rewards ADD COLUMN threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '命中该档需要的代理房间流水金币' AFTER rank_no`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if exists, err := r.columnExists(ctx, "agency_opening_applications", "reward_threshold_coin_spent"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD COLUMN reward_threshold_coin_spent BIGINT NOT NULL DEFAULT 0 COMMENT '结算命中的流水阈值' AFTER reward_rank_no`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ensureAgencyOpeningApplicationOwnerOnceIndex(ctx context.Context) error {
|
||||||
|
ownerOnceExists, err := r.indexExists(ctx, "agency_opening_applications", "uk_agency_opening_owner_once")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ownerOnceExists {
|
||||||
|
// 申请资格是用户维度的一次性活动,唯一键先落在 owner_user_id 上,避免新周期或更换 agency 后重复参与。
|
||||||
|
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD UNIQUE KEY uk_agency_opening_owner_once (app_code, agency_owner_user_id)`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oldAgencyCycleUniqueExists, err := r.indexExists(ctx, "agency_opening_applications", "uk_agency_opening_agency_cycle")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if oldAgencyCycleUniqueExists {
|
||||||
|
// 旧唯一键只限制同一周期同一 agency,不能表达“每个用户只能参与一次”,降级成普通查询索引即可。
|
||||||
|
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications DROP INDEX uk_agency_opening_agency_cycle`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
agencyCycleIndexExists, err := r.indexExists(ctx, "agency_opening_applications", "idx_agency_opening_agency_cycle")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !agencyCycleIndexExists {
|
||||||
|
if _, err := r.db.ExecContext(ctx, `ALTER TABLE agency_opening_applications ADD KEY idx_agency_opening_agency_cycle (app_code, cycle_id, agency_id)`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -0,0 +1,628 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/idgen"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
domain "hyapp/services/activity-service/internal/domain/agencyopening"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListAgencyOpeningCycles returns admin configured cycles with reward rows attached.
|
||||||
|
func (r *Repository) ListAgencyOpeningCycles(ctx context.Context, query domain.ListQuery) ([]domain.Cycle, int64, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
where, args := agencyOpeningCycleWhere(ctx, query)
|
||||||
|
var total int64
|
||||||
|
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM agency_opening_cycles `+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 app_code, cycle_id, activity_code, title, status, start_ms, end_ms, min_host_count, max_agency_age_days,
|
||||||
|
created_by_admin_id, updated_by_admin_id, settled_at_ms, created_at_ms, updated_at_ms
|
||||||
|
FROM agency_opening_cycles `+where+`
|
||||||
|
ORDER BY start_ms DESC, cycle_id DESC
|
||||||
|
LIMIT ? OFFSET ?`, append(args, int(pageSize), int((page-1)*pageSize))...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
cycles := make([]domain.Cycle, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
cycle, scanErr := scanAgencyOpeningCycle(rows)
|
||||||
|
if scanErr != nil {
|
||||||
|
return nil, 0, scanErr
|
||||||
|
}
|
||||||
|
cycles = append(cycles, cycle)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if err := r.attachAgencyOpeningRewards(ctx, cycles); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return cycles, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetAgencyOpeningCycle(ctx context.Context, cycleID string) (domain.Cycle, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return domain.Cycle{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
cycle, err := r.getAgencyOpeningCycle(ctx, r.db, cycleID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Cycle{}, err
|
||||||
|
}
|
||||||
|
cycles := []domain.Cycle{cycle}
|
||||||
|
if err := r.attachAgencyOpeningRewards(ctx, cycles); err != nil {
|
||||||
|
return domain.Cycle{}, err
|
||||||
|
}
|
||||||
|
return cycles[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) UpsertAgencyOpeningCycle(ctx context.Context, command domain.CycleCommand, nowMS int64) (domain.Cycle, bool, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return domain.Cycle{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Cycle{}, false, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
cycle := command.Cycle
|
||||||
|
cycle.AppCode = appcode.FromContext(ctx)
|
||||||
|
cycle.ActivityCode = domain.ActivityCode
|
||||||
|
if cycle.Status == "" {
|
||||||
|
cycle.Status = domain.StatusDraft
|
||||||
|
}
|
||||||
|
created := strings.TrimSpace(cycle.CycleID) == ""
|
||||||
|
if created {
|
||||||
|
cycle.CycleID = idgen.New("aopen")
|
||||||
|
cycle.CreatedAtMS = nowMS
|
||||||
|
cycle.CreatedByAdminID = command.OperatorAdminID
|
||||||
|
} else if _, err := r.getAgencyOpeningCycle(ctx, tx, cycle.CycleID); err != nil {
|
||||||
|
return domain.Cycle{}, false, err
|
||||||
|
}
|
||||||
|
if !created && command.RequireNewRecord {
|
||||||
|
return domain.Cycle{}, false, xerr.New(xerr.InvalidArgument, "cycle_id already exists")
|
||||||
|
}
|
||||||
|
cycle.UpdatedAtMS = nowMS
|
||||||
|
cycle.UpdatedByAdminID = command.OperatorAdminID
|
||||||
|
if err := r.checkAgencyOpeningCycleOverlap(ctx, tx, cycle); err != nil {
|
||||||
|
return domain.Cycle{}, false, err
|
||||||
|
}
|
||||||
|
if created {
|
||||||
|
_, err = tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO agency_opening_cycles (
|
||||||
|
app_code, cycle_id, activity_code, title, status, start_ms, end_ms, min_host_count, max_agency_age_days,
|
||||||
|
created_by_admin_id, updated_by_admin_id, settled_at_ms, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`,
|
||||||
|
cycle.AppCode, cycle.CycleID, cycle.ActivityCode, cycle.Title, cycle.Status, cycle.StartMS, cycle.EndMS, cycle.MinHostCount, cycle.MaxAgencyAgeDays,
|
||||||
|
cycle.CreatedByAdminID, cycle.UpdatedByAdminID, cycle.CreatedAtMS, cycle.UpdatedAtMS)
|
||||||
|
} else {
|
||||||
|
_, err = tx.ExecContext(ctx, `
|
||||||
|
UPDATE agency_opening_cycles
|
||||||
|
SET title = ?, status = ?, start_ms = ?, end_ms = ?, min_host_count = ?, max_agency_age_days = ?,
|
||||||
|
updated_by_admin_id = ?, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND cycle_id = ?`,
|
||||||
|
cycle.Title, cycle.Status, cycle.StartMS, cycle.EndMS, cycle.MinHostCount, cycle.MaxAgencyAgeDays,
|
||||||
|
cycle.UpdatedByAdminID, cycle.UpdatedAtMS, cycle.AppCode, cycle.CycleID)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return domain.Cycle{}, false, err
|
||||||
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx, `DELETE FROM agency_opening_cycle_rewards WHERE app_code = ? AND cycle_id = ?`, cycle.AppCode, cycle.CycleID); err != nil {
|
||||||
|
return domain.Cycle{}, false, err
|
||||||
|
}
|
||||||
|
for _, reward := range cycle.Rewards {
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO agency_opening_cycle_rewards (app_code, cycle_id, rank_no, threshold_coin_spent, reward_coin_amount, created_at_ms, updated_at_ms)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
cycle.AppCode, cycle.CycleID, reward.RankNo, reward.ThresholdCoinSpent, reward.RewardCoinAmount, nowMS, nowMS); err != nil {
|
||||||
|
return domain.Cycle{}, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return domain.Cycle{}, false, err
|
||||||
|
}
|
||||||
|
stored, err := r.GetAgencyOpeningCycle(ctx, cycle.CycleID)
|
||||||
|
return stored, created, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) SetAgencyOpeningCycleStatus(ctx context.Context, cycleID string, status string, operatorAdminID int64, nowMS int64) (domain.Cycle, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return domain.Cycle{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Cycle{}, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
cycle, err := r.getAgencyOpeningCycle(ctx, tx, cycleID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Cycle{}, err
|
||||||
|
}
|
||||||
|
cycle.Status = status
|
||||||
|
if err := r.checkAgencyOpeningCycleOverlap(ctx, tx, cycle); err != nil {
|
||||||
|
return domain.Cycle{}, err
|
||||||
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
UPDATE agency_opening_cycles
|
||||||
|
SET status = ?, updated_by_admin_id = ?, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND cycle_id = ?`,
|
||||||
|
status, operatorAdminID, nowMS, appcode.FromContext(ctx), cycleID); err != nil {
|
||||||
|
return domain.Cycle{}, err
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return domain.Cycle{}, err
|
||||||
|
}
|
||||||
|
return r.GetAgencyOpeningCycle(ctx, cycleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) FindCurrentAgencyOpeningCycle(ctx context.Context, nowMS int64) (domain.Cycle, bool, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return domain.Cycle{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
cycle, err := scanAgencyOpeningCycle(r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT app_code, cycle_id, activity_code, title, status, start_ms, end_ms, min_host_count, max_agency_age_days,
|
||||||
|
created_by_admin_id, updated_by_admin_id, settled_at_ms, created_at_ms, updated_at_ms
|
||||||
|
FROM agency_opening_cycles
|
||||||
|
WHERE app_code = ? AND activity_code = ? AND status = ? AND start_ms <= ? AND end_ms > ?
|
||||||
|
ORDER BY start_ms DESC, updated_at_ms DESC
|
||||||
|
LIMIT 1`, appcode.FromContext(ctx), domain.ActivityCode, domain.StatusActive, nowMS, nowMS))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return domain.Cycle{}, false, nil
|
||||||
|
}
|
||||||
|
return domain.Cycle{}, false, err
|
||||||
|
}
|
||||||
|
cycles := []domain.Cycle{cycle}
|
||||||
|
if err := r.attachAgencyOpeningRewards(ctx, cycles); err != nil {
|
||||||
|
return domain.Cycle{}, false, err
|
||||||
|
}
|
||||||
|
return cycles[0], true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetAgencyOpeningApplicationByOwner(ctx context.Context, ownerUserID int64) (domain.Application, bool, error) {
|
||||||
|
item, err := scanAgencyOpeningApplication(r.db.QueryRowContext(ctx, agencyOpeningApplicationSelect()+`
|
||||||
|
WHERE app_code = ? AND agency_owner_user_id = ?
|
||||||
|
ORDER BY applied_at_ms DESC
|
||||||
|
LIMIT 1`, appcode.FromContext(ctx), ownerUserID))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return domain.Application{}, false, nil
|
||||||
|
}
|
||||||
|
return domain.Application{}, false, err
|
||||||
|
}
|
||||||
|
return item, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) CreateAgencyOpeningApplication(ctx context.Context, cycle domain.Cycle, agency domain.AgencySnapshot, nowMS int64) (domain.Application, bool, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return domain.Application{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
applicationID := idgen.New("aopenapp")
|
||||||
|
result, err := r.db.ExecContext(ctx, `
|
||||||
|
INSERT IGNORE INTO agency_opening_applications (
|
||||||
|
app_code, application_id, cycle_id, agency_id, agency_owner_user_id, agency_name, host_count, agency_created_at_ms,
|
||||||
|
status, score_coin_amount, reward_rank_no, reward_threshold_coin_spent, reward_coin_amount, wallet_command_id, wallet_transaction_id, failure_reason,
|
||||||
|
applied_at_ms, granted_at_ms, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, 0, '', '', '', ?, 0, ?, ?)`,
|
||||||
|
appcode.FromContext(ctx), applicationID, cycle.CycleID, agency.AgencyID, agency.OwnerUserID, agency.Name, agency.HostCount, agency.AgencyCreatedAtMS,
|
||||||
|
domain.ApplicationStatusApplied, nowMS, nowMS, nowMS)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Application{}, false, err
|
||||||
|
}
|
||||||
|
affected, _ := result.RowsAffected()
|
||||||
|
item, ok, err := r.GetAgencyOpeningApplicationByOwner(ctx, agency.OwnerUserID)
|
||||||
|
return item, affected > 0 && ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ListAgencyOpeningApplications(ctx context.Context, query domain.ApplicationQuery) ([]domain.Application, int64, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
where, args := agencyOpeningApplicationWhere(ctx, query)
|
||||||
|
var total int64
|
||||||
|
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM agency_opening_applications `+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, agencyOpeningApplicationSelect()+` `+where+`
|
||||||
|
ORDER BY score_coin_amount DESC, applied_at_ms ASC, agency_id ASC
|
||||||
|
LIMIT ? OFFSET ?`, append(args, int(pageSize), int((page-1)*pageSize))...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := make([]domain.Application, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
item, scanErr := scanAgencyOpeningApplication(rows)
|
||||||
|
if scanErr != nil {
|
||||||
|
return nil, 0, scanErr
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
return items, total, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ConsumeAgencyOpeningGiftEvent(ctx context.Context, event domain.GiftEvent, nowMS int64) (domain.EventResult, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return domain.EventResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return domain.EventResult{}, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
var app domain.Application
|
||||||
|
err = tx.QueryRowContext(ctx, `
|
||||||
|
SELECT app.app_code, app.application_id, app.cycle_id, app.agency_id, app.agency_owner_user_id, app.agency_name, app.host_count, app.agency_created_at_ms,
|
||||||
|
app.status, app.score_coin_amount, app.reward_rank_no, app.reward_threshold_coin_spent, app.reward_coin_amount, app.wallet_command_id, app.wallet_transaction_id, app.failure_reason,
|
||||||
|
app.applied_at_ms, app.granted_at_ms, app.created_at_ms, app.updated_at_ms
|
||||||
|
FROM agency_opening_applications app
|
||||||
|
INNER JOIN agency_opening_cycles cycle ON cycle.app_code = app.app_code AND cycle.cycle_id = app.cycle_id
|
||||||
|
WHERE app.app_code = ? AND app.agency_owner_user_id = ? AND app.status IN (?, ?, ?) AND app.applied_at_ms <= ?
|
||||||
|
AND cycle.activity_code = ? AND cycle.status IN (?, ?) AND cycle.start_ms <= ? AND cycle.end_ms > ?
|
||||||
|
ORDER BY app.applied_at_ms DESC
|
||||||
|
LIMIT 1`, appcode.FromContext(ctx), event.RoomOwnerUserID, domain.ApplicationStatusApplied, domain.ApplicationStatusPending, domain.ApplicationStatusFailed, event.OccurredAtMS,
|
||||||
|
domain.ActivityCode, domain.StatusActive, domain.StatusSettling, event.OccurredAtMS, event.OccurredAtMS).Scan(
|
||||||
|
&app.AppCode, &app.ApplicationID, &app.CycleID, &app.AgencyID, &app.AgencyOwnerUserID, &app.AgencyName,
|
||||||
|
&app.HostCount, &app.AgencyCreatedAtMS, &app.Status, &app.ScoreCoinAmount, &app.RewardRankNo, &app.RewardThresholdCoin, &app.RewardCoinAmount,
|
||||||
|
&app.WalletCommandID, &app.WalletTransactionID, &app.FailureReason, &app.AppliedAtMS, &app.GrantedAtMS, &app.CreatedAtMS, &app.UpdatedAtMS)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return domain.EventResult{EventID: event.EventID, Status: domain.EventStatusSkipped}, tx.Commit()
|
||||||
|
}
|
||||||
|
return domain.EventResult{}, err
|
||||||
|
}
|
||||||
|
result, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT IGNORE INTO agency_opening_score_events (
|
||||||
|
app_code, source_event_id, cycle_id, application_id, agency_id, target_user_id, score_delta, occurred_at_ms, created_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
appcode.FromContext(ctx), event.EventID, app.CycleID, app.ApplicationID, app.AgencyID, event.TargetUserID, event.CoinSpent, event.OccurredAtMS, nowMS)
|
||||||
|
if err != nil {
|
||||||
|
return domain.EventResult{}, err
|
||||||
|
}
|
||||||
|
affected, _ := result.RowsAffected()
|
||||||
|
if affected == 0 {
|
||||||
|
return domain.EventResult{EventID: event.EventID, Status: domain.EventStatusDuplicate}, tx.Commit()
|
||||||
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
UPDATE agency_opening_applications
|
||||||
|
SET score_coin_amount = score_coin_amount + ?, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND application_id = ?`,
|
||||||
|
event.CoinSpent, nowMS, appcode.FromContext(ctx), app.ApplicationID); err != nil {
|
||||||
|
return domain.EventResult{}, err
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return domain.EventResult{}, err
|
||||||
|
}
|
||||||
|
return domain.EventResult{EventID: event.EventID, Status: domain.EventStatusConsumed}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ClaimDueAgencyOpeningCycles(ctx context.Context, workerID string, nowMS int64, lockTTL time.Duration, batchSize int32) ([]domain.Cycle, error) {
|
||||||
|
if batchSize <= 0 {
|
||||||
|
batchSize = 50
|
||||||
|
}
|
||||||
|
lockUntil := nowMS + lockTTL.Milliseconds()
|
||||||
|
if lockTTL <= 0 {
|
||||||
|
lockUntil = nowMS + int64((30 * time.Second).Milliseconds())
|
||||||
|
}
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
UPDATE agency_opening_cycles
|
||||||
|
SET status = ?, locked_by = ?, lock_until_ms = ?, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND activity_code = ? AND status IN (?, ?) AND end_ms <= ? AND (lock_until_ms = 0 OR lock_until_ms <= ?)
|
||||||
|
ORDER BY end_ms ASC, cycle_id ASC
|
||||||
|
LIMIT ?`,
|
||||||
|
domain.StatusSettling, strings.TrimSpace(workerID), lockUntil, nowMS,
|
||||||
|
appcode.FromContext(ctx), domain.ActivityCode, domain.StatusActive, domain.StatusSettling, nowMS, nowMS, int(batchSize))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT app_code, cycle_id, activity_code, title, status, start_ms, end_ms, min_host_count, max_agency_age_days,
|
||||||
|
created_by_admin_id, updated_by_admin_id, settled_at_ms, created_at_ms, updated_at_ms
|
||||||
|
FROM agency_opening_cycles
|
||||||
|
WHERE app_code = ? AND activity_code = ? AND status = ? AND locked_by = ? AND lock_until_ms > ?
|
||||||
|
ORDER BY end_ms ASC, cycle_id ASC
|
||||||
|
LIMIT ?`, appcode.FromContext(ctx), domain.ActivityCode, domain.StatusSettling, strings.TrimSpace(workerID), nowMS, int(batchSize))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
cycles := make([]domain.Cycle, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
cycle, scanErr := scanAgencyOpeningCycle(rows)
|
||||||
|
if scanErr != nil {
|
||||||
|
return nil, scanErr
|
||||||
|
}
|
||||||
|
cycles = append(cycles, cycle)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cycles, r.attachAgencyOpeningRewards(ctx, cycles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) PrepareAgencyOpeningSettlements(ctx context.Context, cycle domain.Cycle, nowMS int64) ([]domain.Application, error) {
|
||||||
|
rewards := activeAgencyOpeningRewardTiers(cycle.Rewards)
|
||||||
|
if len(rewards) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
rows, err := tx.QueryContext(ctx, agencyOpeningApplicationSelect()+`
|
||||||
|
WHERE app_code = ? AND cycle_id = ? AND status IN (?, ?)
|
||||||
|
ORDER BY score_coin_amount DESC, applied_at_ms ASC, agency_id ASC`,
|
||||||
|
appcode.FromContext(ctx), cycle.CycleID, domain.ApplicationStatusApplied, domain.ApplicationStatusFailed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
applications := make([]domain.Application, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
item, scanErr := scanAgencyOpeningApplication(rows)
|
||||||
|
if scanErr != nil {
|
||||||
|
rows.Close()
|
||||||
|
return nil, scanErr
|
||||||
|
}
|
||||||
|
applications = append(applications, item)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, application := range applications {
|
||||||
|
reward, matched := matchAgencyOpeningRewardTier(rewards, application.ScoreCoinAmount)
|
||||||
|
if !matched {
|
||||||
|
reward = domain.Reward{}
|
||||||
|
}
|
||||||
|
walletCommandID := fmt.Sprintf("agency_opening:%s:%s", cycle.CycleID, application.ApplicationID)
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
UPDATE agency_opening_applications
|
||||||
|
SET status = CASE WHEN status = ? THEN status ELSE ? END,
|
||||||
|
reward_rank_no = ?, reward_threshold_coin_spent = ?, reward_coin_amount = ?, wallet_command_id = ?, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND application_id = ? AND status IN (?, ?)`,
|
||||||
|
domain.ApplicationStatusGranted, domain.ApplicationStatusPending,
|
||||||
|
reward.RankNo, reward.ThresholdCoinSpent, reward.RewardCoinAmount, walletCommandID, nowMS,
|
||||||
|
appcode.FromContext(ctx), application.ApplicationID, domain.ApplicationStatusApplied, domain.ApplicationStatusFailed); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pendingApplications, _, err := r.ListAgencyOpeningApplications(ctx, domain.ApplicationQuery{CycleID: cycle.CycleID, Status: domain.ApplicationStatusPending, Page: 1, PageSize: 100})
|
||||||
|
return pendingApplications, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) MarkAgencyOpeningApplicationGranted(ctx context.Context, applicationID string, walletTransactionID string, grantedAtMS int64) (domain.Application, error) {
|
||||||
|
if _, err := r.db.ExecContext(ctx, `
|
||||||
|
UPDATE agency_opening_applications
|
||||||
|
SET status = ?, wallet_transaction_id = ?, failure_reason = '', granted_at_ms = ?, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND application_id = ?`,
|
||||||
|
domain.ApplicationStatusGranted, strings.TrimSpace(walletTransactionID), grantedAtMS, grantedAtMS, appcode.FromContext(ctx), strings.TrimSpace(applicationID)); err != nil {
|
||||||
|
return domain.Application{}, err
|
||||||
|
}
|
||||||
|
return r.getAgencyOpeningApplication(ctx, applicationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) MarkAgencyOpeningApplicationFailed(ctx context.Context, applicationID string, reason string, nowMS int64) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
UPDATE agency_opening_applications
|
||||||
|
SET status = ?, failure_reason = ?, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND application_id = ?`,
|
||||||
|
domain.ApplicationStatusFailed, truncateAgencyOpeningFailure(reason), nowMS, appcode.FromContext(ctx), strings.TrimSpace(applicationID))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateString(value string, limit int) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if limit <= 0 || len(value) <= limit {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) FinishAgencyOpeningCycleIfComplete(ctx context.Context, cycleID string, nowMS int64) (bool, error) {
|
||||||
|
var pending int64
|
||||||
|
if err := r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT COUNT(*) FROM agency_opening_applications
|
||||||
|
WHERE app_code = ? AND cycle_id = ? AND status IN (?, ?)`,
|
||||||
|
appcode.FromContext(ctx), strings.TrimSpace(cycleID), domain.ApplicationStatusPending, domain.ApplicationStatusFailed).Scan(&pending); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if pending > 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
UPDATE agency_opening_cycles
|
||||||
|
SET status = ?, settled_at_ms = ?, locked_by = '', lock_until_ms = 0, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND cycle_id = ?`,
|
||||||
|
domain.StatusSettled, nowMS, nowMS, appcode.FromContext(ctx), strings.TrimSpace(cycleID))
|
||||||
|
return err == nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) getAgencyOpeningApplication(ctx context.Context, applicationID string) (domain.Application, error) {
|
||||||
|
return scanAgencyOpeningApplication(r.db.QueryRowContext(ctx, agencyOpeningApplicationSelect()+` WHERE app_code = ? AND application_id = ?`, appcode.FromContext(ctx), strings.TrimSpace(applicationID)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) getAgencyOpeningCycle(ctx context.Context, q interface {
|
||||||
|
QueryRowContext(context.Context, string, ...any) *sql.Row
|
||||||
|
}, cycleID string) (domain.Cycle, error) {
|
||||||
|
cycleID = strings.TrimSpace(cycleID)
|
||||||
|
if cycleID == "" {
|
||||||
|
return domain.Cycle{}, xerr.New(xerr.InvalidArgument, "cycle_id is required")
|
||||||
|
}
|
||||||
|
return scanAgencyOpeningCycle(q.QueryRowContext(ctx, `
|
||||||
|
SELECT app_code, cycle_id, activity_code, title, status, start_ms, end_ms, min_host_count, max_agency_age_days,
|
||||||
|
created_by_admin_id, updated_by_admin_id, settled_at_ms, created_at_ms, updated_at_ms
|
||||||
|
FROM agency_opening_cycles
|
||||||
|
WHERE app_code = ? AND cycle_id = ?`, appcode.FromContext(ctx), cycleID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) attachAgencyOpeningRewards(ctx context.Context, cycles []domain.Cycle) error {
|
||||||
|
if len(cycles) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ids := make([]string, 0, len(cycles))
|
||||||
|
index := make(map[string]int, len(cycles))
|
||||||
|
for i, cycle := range cycles {
|
||||||
|
ids = append(ids, cycle.CycleID)
|
||||||
|
index[cycle.CycleID] = i
|
||||||
|
}
|
||||||
|
args := []any{appcode.FromContext(ctx)}
|
||||||
|
for _, id := range ids {
|
||||||
|
args = append(args, id)
|
||||||
|
}
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT app_code, cycle_id, rank_no, threshold_coin_spent, reward_coin_amount, created_at_ms, updated_at_ms
|
||||||
|
FROM agency_opening_cycle_rewards
|
||||||
|
WHERE app_code = ? AND cycle_id IN (`+placeholders(len(ids))+`)
|
||||||
|
ORDER BY rank_no ASC`, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var reward domain.Reward
|
||||||
|
if err := rows.Scan(&reward.AppCode, &reward.CycleID, &reward.RankNo, &reward.ThresholdCoinSpent, &reward.RewardCoinAmount, &reward.CreatedAtMS, &reward.UpdatedAtMS); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if i, ok := index[reward.CycleID]; ok {
|
||||||
|
cycles[i].Rewards = append(cycles[i].Rewards, reward)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) checkAgencyOpeningCycleOverlap(ctx context.Context, q interface {
|
||||||
|
QueryRowContext(context.Context, string, ...any) *sql.Row
|
||||||
|
}, cycle domain.Cycle) error {
|
||||||
|
if cycle.Status != domain.StatusActive {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
if err := q.QueryRowContext(ctx, `
|
||||||
|
SELECT COUNT(*) FROM agency_opening_cycles
|
||||||
|
WHERE app_code = ? AND activity_code = ? AND status = ? AND cycle_id <> ? AND start_ms < ? AND end_ms > ?`,
|
||||||
|
appcode.FromContext(ctx), domain.ActivityCode, domain.StatusActive, cycle.CycleID, cycle.EndMS, cycle.StartMS).Scan(&count); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return xerr.New(xerr.Conflict, "active agency opening cycle overlaps")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningCycleWhere(ctx context.Context, query domain.ListQuery) (string, []any) {
|
||||||
|
where := []string{"app_code = ?", "activity_code = ?"}
|
||||||
|
args := []any{appcode.FromContext(ctx), domain.ActivityCode}
|
||||||
|
if strings.TrimSpace(query.Status) != "" {
|
||||||
|
where = append(where, "status = ?")
|
||||||
|
args = append(args, strings.TrimSpace(query.Status))
|
||||||
|
}
|
||||||
|
if query.StartMS > 0 {
|
||||||
|
where = append(where, "end_ms > ?")
|
||||||
|
args = append(args, query.StartMS)
|
||||||
|
}
|
||||||
|
if query.EndMS > 0 {
|
||||||
|
where = append(where, "start_ms < ?")
|
||||||
|
args = append(args, query.EndMS)
|
||||||
|
}
|
||||||
|
return "WHERE " + strings.Join(where, " AND "), args
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningApplicationWhere(ctx context.Context, query domain.ApplicationQuery) (string, []any) {
|
||||||
|
where := []string{"app_code = ?"}
|
||||||
|
args := []any{appcode.FromContext(ctx)}
|
||||||
|
if strings.TrimSpace(query.CycleID) != "" {
|
||||||
|
where = append(where, "cycle_id = ?")
|
||||||
|
args = append(args, strings.TrimSpace(query.CycleID))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(query.Status) != "" {
|
||||||
|
where = append(where, "status = ?")
|
||||||
|
args = append(args, strings.TrimSpace(query.Status))
|
||||||
|
}
|
||||||
|
if query.AgencyID > 0 {
|
||||||
|
where = append(where, "agency_id = ?")
|
||||||
|
args = append(args, query.AgencyID)
|
||||||
|
}
|
||||||
|
if query.AgencyOwnerUserID > 0 {
|
||||||
|
where = append(where, "agency_owner_user_id = ?")
|
||||||
|
args = append(args, query.AgencyOwnerUserID)
|
||||||
|
}
|
||||||
|
return "WHERE " + strings.Join(where, " AND "), args
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningApplicationSelect() string {
|
||||||
|
return `SELECT app_code, application_id, cycle_id, agency_id, agency_owner_user_id, agency_name, host_count, agency_created_at_ms,
|
||||||
|
status, score_coin_amount, reward_rank_no, reward_threshold_coin_spent, reward_coin_amount, wallet_command_id, wallet_transaction_id, failure_reason,
|
||||||
|
applied_at_ms, granted_at_ms, created_at_ms, updated_at_ms FROM agency_opening_applications`
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanAgencyOpeningCycle(row interface{ Scan(...any) error }) (domain.Cycle, error) {
|
||||||
|
var item domain.Cycle
|
||||||
|
err := row.Scan(&item.AppCode, &item.CycleID, &item.ActivityCode, &item.Title, &item.Status, &item.StartMS, &item.EndMS,
|
||||||
|
&item.MinHostCount, &item.MaxAgencyAgeDays, &item.CreatedByAdminID, &item.UpdatedByAdminID, &item.SettledAtMS,
|
||||||
|
&item.CreatedAtMS, &item.UpdatedAtMS)
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanAgencyOpeningApplication(row interface{ Scan(...any) error }) (domain.Application, error) {
|
||||||
|
var item domain.Application
|
||||||
|
err := row.Scan(&item.AppCode, &item.ApplicationID, &item.CycleID, &item.AgencyID, &item.AgencyOwnerUserID, &item.AgencyName,
|
||||||
|
&item.HostCount, &item.AgencyCreatedAtMS, &item.Status, &item.ScoreCoinAmount, &item.RewardRankNo, &item.RewardThresholdCoin, &item.RewardCoinAmount,
|
||||||
|
&item.WalletCommandID, &item.WalletTransactionID, &item.FailureReason, &item.AppliedAtMS, &item.GrantedAtMS, &item.CreatedAtMS, &item.UpdatedAtMS)
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeAgencyOpeningRewardTiers(rewards []domain.Reward) []domain.Reward {
|
||||||
|
out := make([]domain.Reward, 0, len(rewards))
|
||||||
|
for _, reward := range rewards {
|
||||||
|
if reward.RankNo > 0 && reward.ThresholdCoinSpent > 0 && reward.RewardCoinAmount >= 0 {
|
||||||
|
out = append(out, reward)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchAgencyOpeningRewardTier(rewards []domain.Reward, scoreCoinAmount int64) (domain.Reward, bool) {
|
||||||
|
var matched domain.Reward
|
||||||
|
found := false
|
||||||
|
for _, reward := range rewards {
|
||||||
|
if scoreCoinAmount >= reward.ThresholdCoinSpent && (!found || reward.ThresholdCoinSpent > matched.ThresholdCoinSpent) {
|
||||||
|
matched = reward
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matched, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateAgencyOpeningFailure(value string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if len(value) <= 512 {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value[:512]
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
func (r *Repository) ensureCPWeeklyRankTables(ctx context.Context) error {
|
||||||
|
statements := []string{
|
||||||
|
`CREATE TABLE IF NOT EXISTS cp_weekly_rank_configs (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
activity_code VARCHAR(64) NOT NULL DEFAULT 'cp_weekly_rank' COMMENT '活动编码',
|
||||||
|
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||||
|
relation_type VARCHAR(32) NOT NULL DEFAULT 'cp' COMMENT '关系类型,当前固定 cp',
|
||||||
|
top_count INT NOT NULL DEFAULT 3 COMMENT '结算 Top 数量',
|
||||||
|
updated_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',
|
||||||
|
PRIMARY KEY (app_code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='CP 周榜配置表'`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS cp_weekly_rank_rewards (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
rank_no INT NOT NULL COMMENT '名次',
|
||||||
|
resource_group_id BIGINT NOT NULL COMMENT '奖励资源组 ID',
|
||||||
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (app_code, rank_no)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='CP 周榜名次奖励资源组表'`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS cp_weekly_rank_settlements (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||||
|
settlement_id VARCHAR(96) NOT NULL COMMENT '发奖记录 ID',
|
||||||
|
period_start_ms BIGINT NOT NULL COMMENT '周周期开始,UTC epoch ms',
|
||||||
|
period_end_ms BIGINT NOT NULL COMMENT '周周期结束,UTC epoch ms',
|
||||||
|
rank_no INT NOT NULL COMMENT '名次',
|
||||||
|
relationship_id VARCHAR(128) NOT NULL COMMENT '获奖 CP 关系 ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '获奖用户 ID',
|
||||||
|
score BIGINT NOT NULL DEFAULT 0 COMMENT '关系周榜分数',
|
||||||
|
resource_group_id BIGINT NOT NULL COMMENT '发放资源组 ID',
|
||||||
|
wallet_command_id VARCHAR(192) NOT NULL COMMENT 'wallet 发奖幂等键',
|
||||||
|
wallet_grant_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'wallet grant ID',
|
||||||
|
status VARCHAR(32) NOT NULL COMMENT 'pending/running/granted/failed',
|
||||||
|
failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||||
|
attempt_count 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, settlement_id),
|
||||||
|
UNIQUE KEY uk_cp_weekly_rank_period_user (app_code, period_start_ms, rank_no, relationship_id, user_id),
|
||||||
|
UNIQUE KEY uk_cp_weekly_rank_wallet_command (app_code, wallet_command_id),
|
||||||
|
KEY idx_cp_weekly_rank_pending (app_code, period_start_ms, period_end_ms, status, updated_at_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='CP 周榜发奖事实表'`,
|
||||||
|
}
|
||||||
|
for _, statement := range statements {
|
||||||
|
if _, err := r.db.ExecContext(ctx, statement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -0,0 +1,259 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/idgen"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
domain "hyapp/services/activity-service/internal/domain/cpweeklyrank"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Repository) GetCPWeeklyRankConfig(ctx context.Context) (domain.Config, bool, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return domain.Config{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
appCode := appcode.FromContext(ctx)
|
||||||
|
config, err := scanCPWeeklyRankConfig(r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT app_code, activity_code, enabled, relation_type, top_count, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
FROM cp_weekly_rank_configs
|
||||||
|
WHERE app_code = ?`, appCode))
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return domain.Config{}, false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return domain.Config{}, false, err
|
||||||
|
}
|
||||||
|
rewards, err := r.listCPWeeklyRankRewards(ctx, appCode)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Config{}, false, err
|
||||||
|
}
|
||||||
|
config.Rewards = rewards
|
||||||
|
return config, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) UpdateCPWeeklyRankConfig(ctx context.Context, config domain.Config, nowMS int64) (domain.Config, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return domain.Config{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
appCode := appcode.FromContext(ctx)
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Config{}, err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
// 配置行和奖励行必须一起替换;否则后台保存成功但 cron 读到半套奖励会导致榜单部分名次无法发奖。
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO cp_weekly_rank_configs (
|
||||||
|
app_code, activity_code, enabled, relation_type, top_count, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
activity_code = VALUES(activity_code),
|
||||||
|
enabled = VALUES(enabled),
|
||||||
|
relation_type = VALUES(relation_type),
|
||||||
|
top_count = VALUES(top_count),
|
||||||
|
updated_by_admin_id = VALUES(updated_by_admin_id),
|
||||||
|
updated_at_ms = VALUES(updated_at_ms)`,
|
||||||
|
appCode, domain.ActivityCode, config.Enabled, "cp", config.TopCount, config.UpdatedByAdminID, nowMS, nowMS,
|
||||||
|
); err != nil {
|
||||||
|
return domain.Config{}, err
|
||||||
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx, `DELETE FROM cp_weekly_rank_rewards WHERE app_code = ?`, appCode); err != nil {
|
||||||
|
return domain.Config{}, err
|
||||||
|
}
|
||||||
|
for _, reward := range config.Rewards {
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO cp_weekly_rank_rewards (
|
||||||
|
app_code, rank_no, resource_group_id, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
appCode, reward.RankNo, reward.ResourceGroupID, nowMS, nowMS,
|
||||||
|
); err != nil {
|
||||||
|
return domain.Config{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return domain.Config{}, err
|
||||||
|
}
|
||||||
|
stored, _, err := r.GetCPWeeklyRankConfig(ctx)
|
||||||
|
return stored, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ListCPWeeklyRankSettlements(ctx context.Context, periodStartMS int64, periodEndMS int64) ([]domain.Settlement, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
where := []string{"app_code = ?"}
|
||||||
|
args := []any{appcode.FromContext(ctx)}
|
||||||
|
if periodStartMS > 0 {
|
||||||
|
where = append(where, "period_start_ms = ?")
|
||||||
|
args = append(args, periodStartMS)
|
||||||
|
}
|
||||||
|
if periodEndMS > 0 {
|
||||||
|
where = append(where, "period_end_ms = ?")
|
||||||
|
args = append(args, periodEndMS)
|
||||||
|
}
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT app_code, settlement_id, period_start_ms, period_end_ms, rank_no, relationship_id, user_id, score,
|
||||||
|
resource_group_id, wallet_command_id, wallet_grant_id, status, failure_reason, attempt_count, created_at_ms, updated_at_ms
|
||||||
|
FROM cp_weekly_rank_settlements
|
||||||
|
WHERE `+strings.Join(where, " AND ")+`
|
||||||
|
ORDER BY period_start_ms DESC, rank_no ASC, relationship_id ASC, user_id ASC`, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
return scanCPWeeklyRankSettlements(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) PrepareCPWeeklyRankSettlements(ctx context.Context, config domain.Config, entries []domain.Entry, periodStartMS int64, periodEndMS int64, nowMS int64) ([]domain.Settlement, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
appCode := appcode.FromContext(ctx)
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
var existing int
|
||||||
|
if err := tx.QueryRowContext(ctx, `
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM cp_weekly_rank_settlements
|
||||||
|
WHERE app_code = ? AND period_start_ms = ? AND period_end_ms = ?`,
|
||||||
|
appCode, periodStartMS, periodEndMS,
|
||||||
|
).Scan(&existing); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if existing == 0 {
|
||||||
|
rewardsByRank := make(map[int32]int64, len(config.Rewards))
|
||||||
|
for _, reward := range config.Rewards {
|
||||||
|
rewardsByRank[reward.RankNo] = reward.ResourceGroupID
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
rankNo := int32(entry.Rank)
|
||||||
|
resourceGroupID := rewardsByRank[rankNo]
|
||||||
|
if resourceGroupID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, userID := range []int64{entry.UserA.UserID, entry.UserB.UserID} {
|
||||||
|
if userID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
commandID := fmt.Sprintf("cp_weekly_rank:%d:%d:%s:%d", periodStartMS, rankNo, entry.RelationshipID, userID)
|
||||||
|
// 每个获奖关系拆成两个用户发奖记录;INSERT IGNORE 配合唯一键保证同一周期并发 cron 不会重复固化。
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT IGNORE INTO cp_weekly_rank_settlements (
|
||||||
|
app_code, settlement_id, period_start_ms, period_end_ms, rank_no, relationship_id, user_id, score,
|
||||||
|
resource_group_id, wallet_command_id, status, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
appCode, idgen.New("cpwrs"), periodStartMS, periodEndMS, rankNo, entry.RelationshipID, userID, entry.Score,
|
||||||
|
resourceGroupID, commandID, domain.SettlementStatusPending, nowMS, nowMS,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.ListPendingCPWeeklyRankSettlements(ctx, periodStartMS, periodEndMS, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ListPendingCPWeeklyRankSettlements(ctx context.Context, periodStartMS int64, periodEndMS int64, limit int) ([]domain.Settlement, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT app_code, settlement_id, period_start_ms, period_end_ms, rank_no, relationship_id, user_id, score,
|
||||||
|
resource_group_id, wallet_command_id, wallet_grant_id, status, failure_reason, attempt_count, created_at_ms, updated_at_ms
|
||||||
|
FROM cp_weekly_rank_settlements
|
||||||
|
WHERE app_code = ? AND period_start_ms = ? AND period_end_ms = ? AND status IN (?, ?)
|
||||||
|
ORDER BY rank_no ASC, relationship_id ASC, user_id ASC
|
||||||
|
LIMIT ?`,
|
||||||
|
appcode.FromContext(ctx), periodStartMS, periodEndMS, domain.SettlementStatusPending, domain.SettlementStatusFailed, limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
return scanCPWeeklyRankSettlements(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) MarkCPWeeklyRankSettlementGranted(ctx context.Context, settlementID string, walletGrantID string, nowMS int64) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
UPDATE cp_weekly_rank_settlements
|
||||||
|
SET status = ?, wallet_grant_id = ?, failure_reason = '', updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND settlement_id = ?`,
|
||||||
|
domain.SettlementStatusGranted, walletGrantID, nowMS, appcode.FromContext(ctx), settlementID,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) MarkCPWeeklyRankSettlementFailed(ctx context.Context, settlementID string, reason string, nowMS int64) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
UPDATE cp_weekly_rank_settlements
|
||||||
|
SET status = ?, failure_reason = ?, attempt_count = attempt_count + 1, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND settlement_id = ?`,
|
||||||
|
domain.SettlementStatusFailed, truncateCPWeeklyRankFailure(reason), nowMS, appcode.FromContext(ctx), settlementID,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) listCPWeeklyRankRewards(ctx context.Context, appCode string) ([]domain.Reward, error) {
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT app_code, rank_no, resource_group_id, created_at_ms, updated_at_ms
|
||||||
|
FROM cp_weekly_rank_rewards
|
||||||
|
WHERE app_code = ?
|
||||||
|
ORDER BY rank_no ASC`, appCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
rewards := make([]domain.Reward, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var reward domain.Reward
|
||||||
|
if err := rows.Scan(&reward.AppCode, &reward.RankNo, &reward.ResourceGroupID, &reward.CreatedAtMS, &reward.UpdatedAtMS); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rewards = append(rewards, reward)
|
||||||
|
}
|
||||||
|
return rewards, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanCPWeeklyRankConfig(row interface{ Scan(dest ...any) error }) (domain.Config, error) {
|
||||||
|
var config domain.Config
|
||||||
|
var enabled bool
|
||||||
|
err := row.Scan(&config.AppCode, &config.ActivityCode, &enabled, &config.RelationType, &config.TopCount, &config.UpdatedByAdminID, &config.CreatedAtMS, &config.UpdatedAtMS)
|
||||||
|
config.Enabled = enabled
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanCPWeeklyRankSettlements(rows *sql.Rows) ([]domain.Settlement, error) {
|
||||||
|
items := make([]domain.Settlement, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var item domain.Settlement
|
||||||
|
if err := rows.Scan(
|
||||||
|
&item.AppCode, &item.SettlementID, &item.PeriodStartMS, &item.PeriodEndMS, &item.RankNo, &item.RelationshipID, &item.UserID, &item.Score,
|
||||||
|
&item.ResourceGroupID, &item.WalletCommandID, &item.WalletGrantID, &item.Status, &item.FailureReason, &item.AttemptCount, &item.CreatedAtMS, &item.UpdatedAtMS,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
return items, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateCPWeeklyRankFailure(value string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if len(value) > 512 {
|
||||||
|
return value[:512]
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
@ -69,6 +69,9 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
|||||||
if err := r.ensureWeeklyStarTables(ctx); err != nil {
|
if err := r.ensureWeeklyStarTables(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := r.ensureAgencyOpeningTables(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := r.ensureLuckyUserStateFlowColumns(ctx); err != nil {
|
if err := r.ensureLuckyUserStateFlowColumns(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -84,6 +87,9 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
|||||||
if err := r.ensureInviteActivityRewardTables(ctx); err != nil {
|
if err := r.ensureInviteActivityRewardTables(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := r.ensureCPWeeklyRankTables(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := r.ensureTaskDefinitionMetadataColumns(ctx); err != nil {
|
if err := r.ensureTaskDefinitionMetadataColumns(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,239 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
domain "hyapp/services/activity-service/internal/domain/agencyopening"
|
||||||
|
service "hyapp/services/activity-service/internal/service/agencyopening"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AgencyOpeningServer struct {
|
||||||
|
activityv1.UnimplementedAgencyOpeningServiceServer
|
||||||
|
svc *service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAgencyOpeningServer(svc *service.Service) *AgencyOpeningServer {
|
||||||
|
return &AgencyOpeningServer{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AgencyOpeningServer) GetAgencyOpeningStatus(ctx context.Context, req *activityv1.GetAgencyOpeningStatusRequest) (*activityv1.GetAgencyOpeningStatusResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
status, err := s.svc.GetStatus(ctx, req.GetUserId())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &activityv1.GetAgencyOpeningStatusResponse{
|
||||||
|
Cycle: agencyOpeningCycleToProto(status.Cycle),
|
||||||
|
Application: agencyOpeningApplicationToProto(status.Application),
|
||||||
|
Enabled: status.Enabled,
|
||||||
|
Eligible: status.Eligible,
|
||||||
|
Joined: status.Joined,
|
||||||
|
IneligibleReason: status.IneligibleReason,
|
||||||
|
ServerTimeMs: status.ServerTimeMS,
|
||||||
|
Agency: agencyOpeningAgencyToProto(status.Agency),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AgencyOpeningServer) ApplyAgencyOpening(ctx context.Context, req *activityv1.ApplyAgencyOpeningRequest) (*activityv1.ApplyAgencyOpeningResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
application, created, err := s.svc.Apply(ctx, req.GetUserId(), req.GetCommandId())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &activityv1.ApplyAgencyOpeningResponse{Application: agencyOpeningApplicationToProto(application), Created: created}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AgencyOpeningServer) ListAgencyOpeningLeaderboard(ctx context.Context, req *activityv1.ListAgencyOpeningApplicationsRequest) (*activityv1.ListAgencyOpeningApplicationsResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
items, total, err := s.svc.ListApplications(ctx, agencyOpeningApplicationQueryFromProto(req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return agencyOpeningApplicationsResponse(items, total), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminAgencyOpeningServer struct {
|
||||||
|
activityv1.UnimplementedAdminAgencyOpeningServiceServer
|
||||||
|
svc *service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminAgencyOpeningServer(svc *service.Service) *AdminAgencyOpeningServer {
|
||||||
|
return &AdminAgencyOpeningServer{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AdminAgencyOpeningServer) ListAgencyOpeningCycles(ctx context.Context, req *activityv1.ListAgencyOpeningCyclesRequest) (*activityv1.ListAgencyOpeningCyclesResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
cycles, total, err := s.svc.ListCycles(ctx, domain.ListQuery{Status: req.GetStatus(), StartMS: req.GetStartMs(), EndMS: req.GetEndMs(), Page: req.GetPage(), PageSize: req.GetPageSize()})
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
resp := &activityv1.ListAgencyOpeningCyclesResponse{Cycles: make([]*activityv1.AgencyOpeningCycle, 0, len(cycles)), Total: total}
|
||||||
|
for _, cycle := range cycles {
|
||||||
|
resp.Cycles = append(resp.Cycles, agencyOpeningCycleToProto(cycle))
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AdminAgencyOpeningServer) CreateAgencyOpeningCycle(ctx context.Context, req *activityv1.UpsertAgencyOpeningCycleRequest) (*activityv1.UpsertAgencyOpeningCycleResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
cycle, created, err := s.svc.CreateCycle(ctx, domain.CycleCommand{Cycle: agencyOpeningCycleFromProto(req.GetCycle()), OperatorAdminID: req.GetOperatorAdminId()})
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &activityv1.UpsertAgencyOpeningCycleResponse{Cycle: agencyOpeningCycleToProto(cycle), Created: created}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AdminAgencyOpeningServer) GetAgencyOpeningCycle(ctx context.Context, req *activityv1.GetAgencyOpeningCycleRequest) (*activityv1.GetAgencyOpeningCycleResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
cycle, err := s.svc.GetCycle(ctx, req.GetCycleId())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &activityv1.GetAgencyOpeningCycleResponse{Cycle: agencyOpeningCycleToProto(cycle)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AdminAgencyOpeningServer) UpdateAgencyOpeningCycle(ctx context.Context, req *activityv1.UpsertAgencyOpeningCycleRequest) (*activityv1.UpsertAgencyOpeningCycleResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
cycle, created, err := s.svc.UpdateCycle(ctx, domain.CycleCommand{Cycle: agencyOpeningCycleFromProto(req.GetCycle()), OperatorAdminID: req.GetOperatorAdminId()})
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &activityv1.UpsertAgencyOpeningCycleResponse{Cycle: agencyOpeningCycleToProto(cycle), Created: created}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AdminAgencyOpeningServer) SetAgencyOpeningCycleStatus(ctx context.Context, req *activityv1.SetAgencyOpeningCycleStatusRequest) (*activityv1.SetAgencyOpeningCycleStatusResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
cycle, err := s.svc.SetCycleStatus(ctx, req.GetCycleId(), req.GetStatus(), req.GetOperatorAdminId())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &activityv1.SetAgencyOpeningCycleStatusResponse{Cycle: agencyOpeningCycleToProto(cycle)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AdminAgencyOpeningServer) ListAgencyOpeningApplications(ctx context.Context, req *activityv1.ListAgencyOpeningApplicationsRequest) (*activityv1.ListAgencyOpeningApplicationsResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
items, total, err := s.svc.ListApplications(ctx, agencyOpeningApplicationQueryFromProto(req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return agencyOpeningApplicationsResponse(items, total), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningCycleFromProto(item *activityv1.AgencyOpeningCycle) domain.Cycle {
|
||||||
|
if item == nil {
|
||||||
|
return domain.Cycle{}
|
||||||
|
}
|
||||||
|
cycle := domain.Cycle{
|
||||||
|
CycleID: item.GetCycleId(),
|
||||||
|
ActivityCode: item.GetActivityCode(),
|
||||||
|
Title: item.GetTitle(),
|
||||||
|
Status: item.GetStatus(),
|
||||||
|
StartMS: item.GetStartMs(),
|
||||||
|
EndMS: item.GetEndMs(),
|
||||||
|
MinHostCount: item.GetMinHostCount(),
|
||||||
|
MaxAgencyAgeDays: item.GetMaxAgencyAgeDays(),
|
||||||
|
CreatedByAdminID: item.GetCreatedByAdminId(),
|
||||||
|
UpdatedByAdminID: item.GetUpdatedByAdminId(),
|
||||||
|
}
|
||||||
|
for _, reward := range item.GetRewards() {
|
||||||
|
cycle.Rewards = append(cycle.Rewards, domain.Reward{
|
||||||
|
RankNo: reward.GetRankNo(),
|
||||||
|
ThresholdCoinSpent: reward.GetThresholdCoinSpent(),
|
||||||
|
RewardCoinAmount: reward.GetRewardCoinAmount(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cycle
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningCycleToProto(item domain.Cycle) *activityv1.AgencyOpeningCycle {
|
||||||
|
if item.CycleID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resp := &activityv1.AgencyOpeningCycle{
|
||||||
|
AppCode: item.AppCode,
|
||||||
|
CycleId: item.CycleID,
|
||||||
|
ActivityCode: item.ActivityCode,
|
||||||
|
Title: item.Title,
|
||||||
|
Status: item.Status,
|
||||||
|
StartMs: item.StartMS,
|
||||||
|
EndMs: item.EndMS,
|
||||||
|
MinHostCount: item.MinHostCount,
|
||||||
|
MaxAgencyAgeDays: item.MaxAgencyAgeDays,
|
||||||
|
CreatedByAdminId: item.CreatedByAdminID,
|
||||||
|
UpdatedByAdminId: item.UpdatedByAdminID,
|
||||||
|
SettledAtMs: item.SettledAtMS,
|
||||||
|
CreatedAtMs: item.CreatedAtMS,
|
||||||
|
UpdatedAtMs: item.UpdatedAtMS,
|
||||||
|
Rewards: make([]*activityv1.AgencyOpeningReward, 0, len(item.Rewards)),
|
||||||
|
}
|
||||||
|
for _, reward := range item.Rewards {
|
||||||
|
resp.Rewards = append(resp.Rewards, &activityv1.AgencyOpeningReward{
|
||||||
|
RankNo: reward.RankNo,
|
||||||
|
ThresholdCoinSpent: reward.ThresholdCoinSpent,
|
||||||
|
RewardCoinAmount: reward.RewardCoinAmount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningApplicationToProto(item domain.Application) *activityv1.AgencyOpeningApplication {
|
||||||
|
if item.ApplicationID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &activityv1.AgencyOpeningApplication{
|
||||||
|
ApplicationId: item.ApplicationID,
|
||||||
|
CycleId: item.CycleID,
|
||||||
|
AgencyId: item.AgencyID,
|
||||||
|
AgencyOwnerUserId: item.AgencyOwnerUserID,
|
||||||
|
AgencyName: item.AgencyName,
|
||||||
|
HostCount: item.HostCount,
|
||||||
|
AgencyCreatedAtMs: item.AgencyCreatedAtMS,
|
||||||
|
Status: item.Status,
|
||||||
|
ScoreCoinAmount: item.ScoreCoinAmount,
|
||||||
|
RewardRankNo: item.RewardRankNo,
|
||||||
|
RewardThresholdCoinSpent: item.RewardThresholdCoin,
|
||||||
|
RewardCoinAmount: item.RewardCoinAmount,
|
||||||
|
WalletCommandId: item.WalletCommandID,
|
||||||
|
WalletTransactionId: item.WalletTransactionID,
|
||||||
|
FailureReason: item.FailureReason,
|
||||||
|
AppliedAtMs: item.AppliedAtMS,
|
||||||
|
GrantedAtMs: item.GrantedAtMS,
|
||||||
|
UpdatedAtMs: item.UpdatedAtMS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningAgencyToProto(item domain.AgencySnapshot) *activityv1.AgencyOpeningAgencySnapshot {
|
||||||
|
if item.AgencyID <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &activityv1.AgencyOpeningAgencySnapshot{
|
||||||
|
AgencyId: item.AgencyID,
|
||||||
|
OwnerUserId: item.OwnerUserID,
|
||||||
|
Name: item.Name,
|
||||||
|
Status: item.Status,
|
||||||
|
HostCount: item.HostCount,
|
||||||
|
AgencyCreatedAtMs: item.AgencyCreatedAtMS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningApplicationsResponse(items []domain.Application, total int64) *activityv1.ListAgencyOpeningApplicationsResponse {
|
||||||
|
resp := &activityv1.ListAgencyOpeningApplicationsResponse{Applications: make([]*activityv1.AgencyOpeningApplication, 0, len(items)), Total: total}
|
||||||
|
for _, item := range items {
|
||||||
|
resp.Applications = append(resp.Applications, agencyOpeningApplicationToProto(item))
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningApplicationQueryFromProto(req *activityv1.ListAgencyOpeningApplicationsRequest) domain.ApplicationQuery {
|
||||||
|
return domain.ApplicationQuery{
|
||||||
|
CycleID: req.GetCycleId(),
|
||||||
|
Status: req.GetStatus(),
|
||||||
|
AgencyID: req.GetAgencyId(),
|
||||||
|
AgencyOwnerUserID: req.GetAgencyOwnerUserId(),
|
||||||
|
Page: req.GetPage(),
|
||||||
|
PageSize: req.GetPageSize(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import (
|
|||||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/pkg/xerr"
|
"hyapp/pkg/xerr"
|
||||||
|
agencyopeningservice "hyapp/services/activity-service/internal/service/agencyopening"
|
||||||
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
||||||
growthservice "hyapp/services/activity-service/internal/service/growth"
|
growthservice "hyapp/services/activity-service/internal/service/growth"
|
||||||
roomturnoverrewardservice "hyapp/services/activity-service/internal/service/roomturnoverreward"
|
roomturnoverrewardservice "hyapp/services/activity-service/internal/service/roomturnoverreward"
|
||||||
@ -25,20 +26,20 @@ type BroadcastServer struct {
|
|||||||
task *taskservice.Service
|
task *taskservice.Service
|
||||||
roomReward *roomturnoverrewardservice.Service
|
roomReward *roomturnoverrewardservice.Service
|
||||||
weeklyStar *weeklystarservice.Service
|
weeklyStar *weeklystarservice.Service
|
||||||
|
agencyOpen *agencyopeningservice.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBroadcastServer 创建播报和 room event 消费共用的 gRPC adapter。
|
// NewBroadcastServer 创建播报和 room event 消费共用的 gRPC adapter。
|
||||||
// room-service 的 outbox 事件只投递一次,但 activity-service 内部有多条独立投影:
|
// room-service 的 outbox 事件只投递一次,但 activity-service 内部有多条独立投影:
|
||||||
// 播报负责 IM 展示,growth 负责等级进度,weekly-star 负责指定礼物榜单,roomReward 负责房间流水。
|
// 播报负责 IM 展示,growth 负责等级进度,weekly-star 负责指定礼物榜单,roomReward 负责房间流水。
|
||||||
// 这里把这些模块都挂到同一个 gRPC 入口,保证本地直连投递和 MQ 投递使用同一组业务副作用。
|
// 这里把这些模块都挂到同一个 gRPC 入口,保证本地直连投递和 MQ 投递使用同一组业务副作用。
|
||||||
func NewBroadcastServer(svc *broadcastservice.Service, growthSvc *growthservice.Service, weeklyStarSvc *weeklystarservice.Service, taskSvc *taskservice.Service, roomRewardSvc ...*roomturnoverrewardservice.Service) *BroadcastServer {
|
func NewBroadcastServer(svc *broadcastservice.Service, growthSvc *growthservice.Service, weeklyStarSvc *weeklystarservice.Service, taskSvc *taskservice.Service, roomRewardSvc *roomturnoverrewardservice.Service, agencyOpeningSvc *agencyopeningservice.Service) *BroadcastServer {
|
||||||
server := &BroadcastServer{svc: svc}
|
server := &BroadcastServer{svc: svc}
|
||||||
server.growth = growthSvc
|
server.growth = growthSvc
|
||||||
server.weeklyStar = weeklyStarSvc
|
server.weeklyStar = weeklyStarSvc
|
||||||
server.task = taskSvc
|
server.task = taskSvc
|
||||||
if len(roomRewardSvc) > 0 {
|
server.roomReward = roomRewardSvc
|
||||||
server.roomReward = roomRewardSvc[0]
|
server.agencyOpen = agencyOpeningSvc
|
||||||
}
|
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,6 +130,11 @@ func (s *BroadcastServer) ConsumeRoomEvent(ctx context.Context, req *activityv1.
|
|||||||
return nil, xerr.ToGRPCError(err)
|
return nil, xerr.ToGRPCError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if s.agencyOpen != nil {
|
||||||
|
if _, err := s.agencyOpen.HandleRoomEvent(ctx, req.GetEnvelope()); err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
if s.task != nil {
|
if s.task != nil {
|
||||||
if _, err := s.task.HandleRoomEvent(ctx, req.GetEnvelope()); err != nil {
|
if _, err := s.task.HandleRoomEvent(ctx, req.GetEnvelope()); err != nil {
|
||||||
return nil, xerr.ToGRPCError(err)
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
|||||||
@ -0,0 +1,176 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
domain "hyapp/services/activity-service/internal/domain/cpweeklyrank"
|
||||||
|
service "hyapp/services/activity-service/internal/service/cpweeklyrank"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CPWeeklyRankServer struct {
|
||||||
|
activityv1.UnimplementedCPWeeklyRankServiceServer
|
||||||
|
activityv1.UnimplementedAdminCPWeeklyRankServiceServer
|
||||||
|
service *service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCPWeeklyRankServer(service *service.Service) *CPWeeklyRankServer {
|
||||||
|
return &CPWeeklyRankServer{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CPWeeklyRankServer) GetCPWeeklyRankStatus(ctx context.Context, req *activityv1.GetCPWeeklyRankStatusRequest) (*activityv1.GetCPWeeklyRankStatusResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
if s.service == nil {
|
||||||
|
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp weekly rank service is not configured"))
|
||||||
|
}
|
||||||
|
status, err := s.service.GetStatus(ctx, req.GetLimit(), req.GetNowMs())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &activityv1.GetCPWeeklyRankStatusResponse{
|
||||||
|
Config: cpWeeklyRankConfigToProto(status.Config),
|
||||||
|
Entries: cpWeeklyRankEntriesToProto(status.Entries),
|
||||||
|
PeriodStartMs: status.PeriodStartMS,
|
||||||
|
PeriodEndMs: status.PeriodEndMS,
|
||||||
|
ServerTimeMs: status.ServerTimeMS,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CPWeeklyRankServer) GetCPWeeklyRankConfig(ctx context.Context, req *activityv1.GetCPWeeklyRankConfigRequest) (*activityv1.GetCPWeeklyRankConfigResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
if s.service == nil {
|
||||||
|
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp weekly rank service is not configured"))
|
||||||
|
}
|
||||||
|
config, err := s.service.GetConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &activityv1.GetCPWeeklyRankConfigResponse{Config: cpWeeklyRankConfigToProto(config)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CPWeeklyRankServer) UpdateCPWeeklyRankConfig(ctx context.Context, req *activityv1.UpdateCPWeeklyRankConfigRequest) (*activityv1.UpdateCPWeeklyRankConfigResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
if s.service == nil {
|
||||||
|
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp weekly rank service is not configured"))
|
||||||
|
}
|
||||||
|
config, err := s.service.UpdateConfig(ctx, cpWeeklyRankConfigFromProto(req.GetConfig()), req.GetOperatorAdminId())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &activityv1.UpdateCPWeeklyRankConfigResponse{Config: cpWeeklyRankConfigToProto(config)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CPWeeklyRankServer) ListCPWeeklyRankSettlements(ctx context.Context, req *activityv1.ListCPWeeklyRankSettlementsRequest) (*activityv1.ListCPWeeklyRankSettlementsResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
if s.service == nil {
|
||||||
|
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp weekly rank service is not configured"))
|
||||||
|
}
|
||||||
|
items, err := s.service.ListSettlements(ctx, req.GetPeriodStartMs(), req.GetPeriodEndMs())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &activityv1.ListCPWeeklyRankSettlementsResponse{Settlements: cpWeeklyRankSettlementsToProto(items)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankConfigFromProto(item *activityv1.CPWeeklyRankConfig) domain.Config {
|
||||||
|
if item == nil {
|
||||||
|
return domain.Config{}
|
||||||
|
}
|
||||||
|
config := domain.Config{
|
||||||
|
AppCode: item.GetAppCode(),
|
||||||
|
ActivityCode: item.GetActivityCode(),
|
||||||
|
Enabled: item.GetEnabled(),
|
||||||
|
RelationType: item.GetRelationType(),
|
||||||
|
TopCount: item.GetTopCount(),
|
||||||
|
UpdatedByAdminID: item.GetUpdatedByAdminId(),
|
||||||
|
CreatedAtMS: item.GetCreatedAtMs(),
|
||||||
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
|
Rewards: make([]domain.Reward, 0, len(item.GetRewards())),
|
||||||
|
}
|
||||||
|
for _, reward := range item.GetRewards() {
|
||||||
|
config.Rewards = append(config.Rewards, domain.Reward{
|
||||||
|
RankNo: reward.GetRankNo(),
|
||||||
|
ResourceGroupID: reward.GetResourceGroupId(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankConfigToProto(item domain.Config) *activityv1.CPWeeklyRankConfig {
|
||||||
|
return &activityv1.CPWeeklyRankConfig{
|
||||||
|
ActivityCode: item.ActivityCode,
|
||||||
|
Enabled: item.Enabled,
|
||||||
|
RelationType: item.RelationType,
|
||||||
|
TopCount: item.TopCount,
|
||||||
|
Rewards: cpWeeklyRankRewardsToProto(item.Rewards),
|
||||||
|
UpdatedByAdminId: item.UpdatedByAdminID,
|
||||||
|
CreatedAtMs: item.CreatedAtMS,
|
||||||
|
UpdatedAtMs: item.UpdatedAtMS,
|
||||||
|
AppCode: item.AppCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankRewardsToProto(items []domain.Reward) []*activityv1.CPWeeklyRankReward {
|
||||||
|
resp := make([]*activityv1.CPWeeklyRankReward, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
resp = append(resp, &activityv1.CPWeeklyRankReward{RankNo: item.RankNo, ResourceGroupId: item.ResourceGroupID})
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankEntriesToProto(items []domain.Entry) []*activityv1.CPWeeklyRankEntry {
|
||||||
|
resp := make([]*activityv1.CPWeeklyRankEntry, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
resp = append(resp, &activityv1.CPWeeklyRankEntry{
|
||||||
|
Rank: item.Rank,
|
||||||
|
RelationshipId: item.RelationshipID,
|
||||||
|
RelationType: item.RelationType,
|
||||||
|
Score: item.Score,
|
||||||
|
UserA: cpWeeklyRankUserToProto(item.UserA),
|
||||||
|
UserB: cpWeeklyRankUserToProto(item.UserB),
|
||||||
|
FormedAtMs: item.FormedAtMS,
|
||||||
|
UpdatedAtMs: item.UpdatedAtMS,
|
||||||
|
FirstScoredAtMs: item.FirstScoredAtMS,
|
||||||
|
LastScoredAtMs: item.LastScoredAtMS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankUserToProto(item domain.User) *activityv1.CPWeeklyRankUser {
|
||||||
|
if item.UserID <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &activityv1.CPWeeklyRankUser{
|
||||||
|
UserId: item.UserID,
|
||||||
|
DisplayUserId: item.DisplayUserID,
|
||||||
|
Username: item.Username,
|
||||||
|
Avatar: item.Avatar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankSettlementsToProto(items []domain.Settlement) []*activityv1.CPWeeklyRankSettlement {
|
||||||
|
resp := make([]*activityv1.CPWeeklyRankSettlement, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
resp = append(resp, &activityv1.CPWeeklyRankSettlement{
|
||||||
|
SettlementId: item.SettlementID,
|
||||||
|
PeriodStartMs: item.PeriodStartMS,
|
||||||
|
PeriodEndMs: item.PeriodEndMS,
|
||||||
|
RankNo: item.RankNo,
|
||||||
|
RelationshipId: item.RelationshipID,
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
@ -8,6 +8,8 @@ import (
|
|||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/pkg/xerr"
|
"hyapp/pkg/xerr"
|
||||||
achievementservice "hyapp/services/activity-service/internal/service/achievement"
|
achievementservice "hyapp/services/activity-service/internal/service/achievement"
|
||||||
|
agencyopeningservice "hyapp/services/activity-service/internal/service/agencyopening"
|
||||||
|
cpweeklyrankservice "hyapp/services/activity-service/internal/service/cpweeklyrank"
|
||||||
growthservice "hyapp/services/activity-service/internal/service/growth"
|
growthservice "hyapp/services/activity-service/internal/service/growth"
|
||||||
messageservice "hyapp/services/activity-service/internal/service/message"
|
messageservice "hyapp/services/activity-service/internal/service/message"
|
||||||
roomturnoverrewardservice "hyapp/services/activity-service/internal/service/roomturnoverreward"
|
roomturnoverrewardservice "hyapp/services/activity-service/internal/service/roomturnoverreward"
|
||||||
@ -23,17 +25,19 @@ type CronServer struct {
|
|||||||
achievement *achievementservice.Service
|
achievement *achievementservice.Service
|
||||||
weeklyStar *weeklystarservice.Service
|
weeklyStar *weeklystarservice.Service
|
||||||
roomReward *roomturnoverrewardservice.Service
|
roomReward *roomturnoverrewardservice.Service
|
||||||
|
agencyOpen *agencyopeningservice.Service
|
||||||
|
cpWeekly *cpweeklyrankservice.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCronServer creates the internal cron adapter without exposing message storage.
|
// 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) *CronServer {
|
func NewCronServer(messageSvc *messageservice.Service, growthSvc *growthservice.Service, achievementSvc *achievementservice.Service, weeklyStarSvc *weeklystarservice.Service, roomRewardSvc *roomturnoverrewardservice.Service, agencyOpeningSvc *agencyopeningservice.Service, cpWeeklyRankSvc *cpweeklyrankservice.Service) *CronServer {
|
||||||
server := &CronServer{message: messageSvc}
|
server := &CronServer{message: messageSvc}
|
||||||
server.growth = growthSvc
|
server.growth = growthSvc
|
||||||
server.achievement = achievementSvc
|
server.achievement = achievementSvc
|
||||||
server.weeklyStar = weeklyStarSvc
|
server.weeklyStar = weeklyStarSvc
|
||||||
if len(roomRewardSvc) > 0 {
|
server.roomReward = roomRewardSvc
|
||||||
server.roomReward = roomRewardSvc[0]
|
server.agencyOpen = agencyOpeningSvc
|
||||||
}
|
server.cpWeekly = cpWeeklyRankSvc
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,6 +123,25 @@ func (s *CronServer) ProcessWeeklyStarSettlementBatch(ctx context.Context, req *
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProcessCPWeeklyRankSettlementBatch closes the previous UTC CP week and grants configured Top rewards.
|
||||||
|
func (s *CronServer) ProcessCPWeeklyRankSettlementBatch(ctx context.Context, req *activityv1.CronBatchRequest) (*activityv1.CronBatchResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
if s.cpWeekly == nil {
|
||||||
|
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp weekly rank service is not configured"))
|
||||||
|
}
|
||||||
|
claimed, processed, success, failure, hasMore, err := s.cpWeekly.ProcessSettlementBatch(ctx, req.GetRunId(), req.GetWorkerId(), req.GetBatchSize())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &activityv1.CronBatchResponse{
|
||||||
|
ClaimedCount: claimed,
|
||||||
|
ProcessedCount: processed,
|
||||||
|
SuccessCount: success,
|
||||||
|
FailureCount: failure,
|
||||||
|
HasMore: hasMore,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProcessRoomTurnoverRewardSettlementBatch closes the previous UTC week and grants pending room rewards.
|
// ProcessRoomTurnoverRewardSettlementBatch closes the previous UTC week and grants pending room rewards.
|
||||||
func (s *CronServer) ProcessRoomTurnoverRewardSettlementBatch(ctx context.Context, req *activityv1.CronBatchRequest) (*activityv1.CronBatchResponse, error) {
|
func (s *CronServer) ProcessRoomTurnoverRewardSettlementBatch(ctx context.Context, req *activityv1.CronBatchRequest) (*activityv1.CronBatchResponse, error) {
|
||||||
// cron-service 只负责调度,activity-service 才拥有上一 UTC 周期认领 settlement 和发奖状态机。
|
// cron-service 只负责调度,activity-service 才拥有上一 UTC 周期认领 settlement 和发奖状态机。
|
||||||
@ -139,6 +162,25 @@ func (s *CronServer) ProcessRoomTurnoverRewardSettlementBatch(ctx context.Contex
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProcessAgencyOpeningSettlementBatch claims ended agency-opening cycles and grants leaderboard COIN rewards.
|
||||||
|
func (s *CronServer) ProcessAgencyOpeningSettlementBatch(ctx context.Context, req *activityv1.CronBatchRequest) (*activityv1.CronBatchResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
|
if s.agencyOpen == nil {
|
||||||
|
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "agency opening service is not configured"))
|
||||||
|
}
|
||||||
|
claimed, processed, success, failure, hasMore, err := s.agencyOpen.ProcessSettlementBatch(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
|
||||||
|
}
|
||||||
|
|
||||||
func durationFromMillis(value int64) time.Duration {
|
func durationFromMillis(value int64) time.Duration {
|
||||||
if value <= 0 {
|
if value <= 0 {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@ -50,6 +50,13 @@ tasks:
|
|||||||
lock_ttl: "1m"
|
lock_ttl: "1m"
|
||||||
batch_size: 50
|
batch_size: 50
|
||||||
app_codes: ["lalu"]
|
app_codes: ["lalu"]
|
||||||
|
agency_opening_settlement:
|
||||||
|
enabled: true
|
||||||
|
interval: "5s"
|
||||||
|
timeout: "20s"
|
||||||
|
lock_ttl: "1m"
|
||||||
|
batch_size: 50
|
||||||
|
app_codes: ["lalu"]
|
||||||
room_turnover_reward_settlement:
|
room_turnover_reward_settlement:
|
||||||
enabled: true
|
enabled: true
|
||||||
interval: "5m"
|
interval: "5m"
|
||||||
|
|||||||
@ -50,6 +50,13 @@ tasks:
|
|||||||
lock_ttl: "1m"
|
lock_ttl: "1m"
|
||||||
batch_size: 50
|
batch_size: 50
|
||||||
app_codes: ["lalu"]
|
app_codes: ["lalu"]
|
||||||
|
agency_opening_settlement:
|
||||||
|
enabled: true
|
||||||
|
interval: "5s"
|
||||||
|
timeout: "20s"
|
||||||
|
lock_ttl: "1m"
|
||||||
|
batch_size: 50
|
||||||
|
app_codes: ["lalu"]
|
||||||
room_turnover_reward_settlement:
|
room_turnover_reward_settlement:
|
||||||
enabled: true
|
enabled: true
|
||||||
interval: "5m"
|
interval: "5m"
|
||||||
|
|||||||
@ -50,6 +50,13 @@ tasks:
|
|||||||
lock_ttl: "1m"
|
lock_ttl: "1m"
|
||||||
batch_size: 50
|
batch_size: 50
|
||||||
app_codes: ["lalu"]
|
app_codes: ["lalu"]
|
||||||
|
agency_opening_settlement:
|
||||||
|
enabled: true
|
||||||
|
interval: "5s"
|
||||||
|
timeout: "20s"
|
||||||
|
lock_ttl: "1m"
|
||||||
|
batch_size: 50
|
||||||
|
app_codes: ["lalu"]
|
||||||
room_turnover_reward_settlement:
|
room_turnover_reward_settlement:
|
||||||
enabled: true
|
enabled: true
|
||||||
interval: "5m"
|
interval: "5m"
|
||||||
|
|||||||
@ -123,6 +123,8 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
"growth_level_reward": activityCron.ProcessLevelRewardBatch,
|
"growth_level_reward": activityCron.ProcessLevelRewardBatch,
|
||||||
"achievement_reward": activityCron.ProcessAchievementRewardBatch,
|
"achievement_reward": activityCron.ProcessAchievementRewardBatch,
|
||||||
"weekly_star_settlement": activityCron.ProcessWeeklyStarSettlementBatch,
|
"weekly_star_settlement": activityCron.ProcessWeeklyStarSettlementBatch,
|
||||||
|
"agency_opening_settlement": activityCron.ProcessAgencyOpeningSettlementBatch,
|
||||||
|
"cp_weekly_rank_settlement": activityCron.ProcessCPWeeklyRankSettlementBatch,
|
||||||
"room_turnover_reward_settlement": activityCron.ProcessRoomTurnoverRewardSettlementBatch,
|
"room_turnover_reward_settlement": activityCron.ProcessRoomTurnoverRewardSettlementBatch,
|
||||||
"game_level_event_relay": gameCron.ProcessLevelEventOutboxBatch,
|
"game_level_event_relay": gameCron.ProcessLevelEventOutboxBatch,
|
||||||
"host_salary_daily_settlement": walletCron.ProcessHostSalaryDailySettlementBatch,
|
"host_salary_daily_settlement": walletCron.ProcessHostSalaryDailySettlementBatch,
|
||||||
|
|||||||
@ -172,6 +172,22 @@ func defaultTasks() map[string]TaskConfig {
|
|||||||
BatchSize: 50,
|
BatchSize: 50,
|
||||||
AppCodes: []string{defaultAppCode},
|
AppCodes: []string{defaultAppCode},
|
||||||
},
|
},
|
||||||
|
"agency_opening_settlement": {
|
||||||
|
Enabled: true,
|
||||||
|
Interval: "5s",
|
||||||
|
Timeout: "20s",
|
||||||
|
LockTTL: "1m",
|
||||||
|
BatchSize: 50,
|
||||||
|
AppCodes: []string{defaultAppCode},
|
||||||
|
},
|
||||||
|
"cp_weekly_rank_settlement": {
|
||||||
|
Enabled: true,
|
||||||
|
Interval: "5m",
|
||||||
|
Timeout: "30s",
|
||||||
|
LockTTL: "5m",
|
||||||
|
BatchSize: 50,
|
||||||
|
AppCodes: []string{defaultAppCode},
|
||||||
|
},
|
||||||
"room_turnover_reward_settlement": {
|
"room_turnover_reward_settlement": {
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Interval: "5m",
|
Interval: "5m",
|
||||||
|
|||||||
@ -54,6 +54,24 @@ func (c *ActivityCronClient) ProcessWeeklyStarSettlementBatch(ctx context.Contex
|
|||||||
return activityCronBatchResult(resp), nil
|
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))
|
||||||
|
if err != nil {
|
||||||
|
return scheduler.BatchResult{}, err
|
||||||
|
}
|
||||||
|
return activityCronBatchResult(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessCPWeeklyRankSettlementBatch handles previous-week CP leaderboard reward settlement.
|
||||||
|
func (c *ActivityCronClient) ProcessCPWeeklyRankSettlementBatch(ctx context.Context, req scheduler.BatchRequest) (scheduler.BatchResult, error) {
|
||||||
|
resp, err := c.client.ProcessCPWeeklyRankSettlementBatch(ctx, activityCronBatchRequest(req))
|
||||||
|
if err != nil {
|
||||||
|
return scheduler.BatchResult{}, err
|
||||||
|
}
|
||||||
|
return activityCronBatchResult(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProcessRoomTurnoverRewardSettlementBatch handles ended room-turnover-reward cycle settlements.
|
// ProcessRoomTurnoverRewardSettlementBatch handles ended room-turnover-reward cycle settlements.
|
||||||
func (c *ActivityCronClient) ProcessRoomTurnoverRewardSettlementBatch(ctx context.Context, req scheduler.BatchRequest) (scheduler.BatchResult, error) {
|
func (c *ActivityCronClient) ProcessRoomTurnoverRewardSettlementBatch(ctx context.Context, req scheduler.BatchRequest) (scheduler.BatchResult, error) {
|
||||||
// cron-service 不直连 activity MySQL;它只把 run_id、worker_id 和 batch_size 传给活动服务,让 owner service 自己处理幂等和状态推进。
|
// cron-service 不直连 activity MySQL;它只把 run_id、worker_id 和 batch_size 传给活动服务,让 owner service 自己处理幂等和状态推进。
|
||||||
|
|||||||
@ -108,6 +108,8 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
var luckyGiftClient client.LuckyGiftClient = client.NewGRPCLuckyGiftClient(activityConn)
|
var luckyGiftClient client.LuckyGiftClient = client.NewGRPCLuckyGiftClient(activityConn)
|
||||||
var roomTurnoverRewardClient client.RoomTurnoverRewardClient = client.NewGRPCRoomTurnoverRewardClient(activityConn)
|
var roomTurnoverRewardClient client.RoomTurnoverRewardClient = client.NewGRPCRoomTurnoverRewardClient(activityConn)
|
||||||
var weeklyStarClient client.WeeklyStarClient = client.NewGRPCWeeklyStarClient(activityConn)
|
var weeklyStarClient client.WeeklyStarClient = client.NewGRPCWeeklyStarClient(activityConn)
|
||||||
|
var cpWeeklyRankClient client.CPWeeklyRankClient = client.NewGRPCCPWeeklyRankClient(activityConn)
|
||||||
|
var agencyOpeningClient client.AgencyOpeningClient = client.NewGRPCAgencyOpeningClient(activityConn)
|
||||||
var broadcastClient client.BroadcastClient = client.NewGRPCBroadcastClient(activityConn)
|
var broadcastClient client.BroadcastClient = client.NewGRPCBroadcastClient(activityConn)
|
||||||
var gameClient client.GameClient = client.NewGRPCGameClient(gameConn)
|
var gameClient client.GameClient = client.NewGRPCGameClient(gameConn)
|
||||||
var statisticsClient client.StatisticsClient = client.NewHTTPStatisticsClient(cfg.Statistics.BaseURL, cfg.Statistics.Timeout)
|
var statisticsClient client.StatisticsClient = client.NewHTTPStatisticsClient(cfg.Statistics.BaseURL, cfg.Statistics.Timeout)
|
||||||
@ -180,6 +182,8 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
handler.SetLuckyGiftClient(luckyGiftClient)
|
handler.SetLuckyGiftClient(luckyGiftClient)
|
||||||
handler.SetRoomTurnoverRewardClient(roomTurnoverRewardClient)
|
handler.SetRoomTurnoverRewardClient(roomTurnoverRewardClient)
|
||||||
handler.SetWeeklyStarClient(weeklyStarClient)
|
handler.SetWeeklyStarClient(weeklyStarClient)
|
||||||
|
handler.SetCPWeeklyRankClient(cpWeeklyRankClient)
|
||||||
|
handler.SetAgencyOpeningClient(agencyOpeningClient)
|
||||||
handler.SetBroadcastClient(broadcastClient)
|
handler.SetBroadcastClient(broadcastClient)
|
||||||
handler.SetGameClient(gameClient)
|
handler.SetGameClient(gameClient)
|
||||||
handler.SetStatisticsClient(statisticsClient)
|
handler.SetStatisticsClient(statisticsClient)
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AgencyOpeningClient abstracts H5 agency opening activity calls to activity-service.
|
||||||
|
type AgencyOpeningClient interface {
|
||||||
|
GetAgencyOpeningStatus(ctx context.Context, req *activityv1.GetAgencyOpeningStatusRequest) (*activityv1.GetAgencyOpeningStatusResponse, error)
|
||||||
|
ApplyAgencyOpening(ctx context.Context, req *activityv1.ApplyAgencyOpeningRequest) (*activityv1.ApplyAgencyOpeningResponse, error)
|
||||||
|
ListAgencyOpeningLeaderboard(ctx context.Context, req *activityv1.ListAgencyOpeningApplicationsRequest) (*activityv1.ListAgencyOpeningApplicationsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type grpcAgencyOpeningClient struct {
|
||||||
|
client activityv1.AgencyOpeningServiceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGRPCAgencyOpeningClient(conn *grpc.ClientConn) AgencyOpeningClient {
|
||||||
|
return &grpcAgencyOpeningClient{client: activityv1.NewAgencyOpeningServiceClient(conn)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcAgencyOpeningClient) GetAgencyOpeningStatus(ctx context.Context, req *activityv1.GetAgencyOpeningStatusRequest) (*activityv1.GetAgencyOpeningStatusResponse, error) {
|
||||||
|
return c.client.GetAgencyOpeningStatus(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcAgencyOpeningClient) ApplyAgencyOpening(ctx context.Context, req *activityv1.ApplyAgencyOpeningRequest) (*activityv1.ApplyAgencyOpeningResponse, error) {
|
||||||
|
return c.client.ApplyAgencyOpening(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcAgencyOpeningClient) ListAgencyOpeningLeaderboard(ctx context.Context, req *activityv1.ListAgencyOpeningApplicationsRequest) (*activityv1.ListAgencyOpeningApplicationsResponse, error) {
|
||||||
|
return c.client.ListAgencyOpeningLeaderboard(ctx, req)
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CPWeeklyRankClient abstracts H5 CP weekly rank reads from activity-service.
|
||||||
|
type CPWeeklyRankClient interface {
|
||||||
|
GetCPWeeklyRankStatus(ctx context.Context, req *activityv1.GetCPWeeklyRankStatusRequest) (*activityv1.GetCPWeeklyRankStatusResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type grpcCPWeeklyRankClient struct {
|
||||||
|
client activityv1.CPWeeklyRankServiceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGRPCCPWeeklyRankClient(conn *grpc.ClientConn) CPWeeklyRankClient {
|
||||||
|
return &grpcCPWeeklyRankClient{client: activityv1.NewCPWeeklyRankServiceClient(conn)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcCPWeeklyRankClient) GetCPWeeklyRankStatus(ctx context.Context, req *activityv1.GetCPWeeklyRankStatusRequest) (*activityv1.GetCPWeeklyRankStatusResponse, error) {
|
||||||
|
return c.client.GetCPWeeklyRankStatus(ctx, req)
|
||||||
|
}
|
||||||
@ -38,6 +38,7 @@ type UserProfileClient interface {
|
|||||||
BatchGetUsers(ctx context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error)
|
BatchGetUsers(ctx context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error)
|
||||||
CompleteOnboarding(ctx context.Context, req *userv1.CompleteOnboardingRequest) (*userv1.CompleteOnboardingResponse, error)
|
CompleteOnboarding(ctx context.Context, req *userv1.CompleteOnboardingRequest) (*userv1.CompleteOnboardingResponse, error)
|
||||||
UpdateUserProfile(ctx context.Context, req *userv1.UpdateUserProfileRequest) (*userv1.UpdateUserProfileResponse, error)
|
UpdateUserProfile(ctx context.Context, req *userv1.UpdateUserProfileRequest) (*userv1.UpdateUserProfileResponse, error)
|
||||||
|
UpdateUserProfileBackground(ctx context.Context, req *userv1.UpdateUserProfileBackgroundRequest) (*userv1.UpdateUserProfileBackgroundResponse, error)
|
||||||
ChangeUserCountry(ctx context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error)
|
ChangeUserCountry(ctx context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error)
|
||||||
CreateManagerUserBlock(ctx context.Context, req *userv1.CreateManagerUserBlockRequest) (*userv1.CreateManagerUserBlockResponse, error)
|
CreateManagerUserBlock(ctx context.Context, req *userv1.CreateManagerUserBlockRequest) (*userv1.CreateManagerUserBlockResponse, error)
|
||||||
ListManagerUserBlocks(ctx context.Context, req *userv1.ListManagerUserBlocksRequest) (*userv1.ListManagerUserBlocksResponse, error)
|
ListManagerUserBlocks(ctx context.Context, req *userv1.ListManagerUserBlocksRequest) (*userv1.ListManagerUserBlocksResponse, error)
|
||||||
@ -324,6 +325,10 @@ func (c *grpcUserProfileClient) UpdateUserProfile(ctx context.Context, req *user
|
|||||||
return c.client.UpdateUserProfile(ctx, req)
|
return c.client.UpdateUserProfile(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *grpcUserProfileClient) UpdateUserProfileBackground(ctx context.Context, req *userv1.UpdateUserProfileBackgroundRequest) (*userv1.UpdateUserProfileBackgroundResponse, error) {
|
||||||
|
return c.client.UpdateUserProfileBackground(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *grpcUserProfileClient) ChangeUserCountry(ctx context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) {
|
func (c *grpcUserProfileClient) ChangeUserCountry(ctx context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) {
|
||||||
return c.client.ChangeUserCountry(ctx, req)
|
return c.client.ChangeUserCountry(ctx, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,336 @@
|
|||||||
|
package activityapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type agencyOpeningApplyBody struct {
|
||||||
|
CommandID string `json:"command_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyOpeningStatusData struct {
|
||||||
|
ServerTimeMS int64 `json:"server_time_ms"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Eligible bool `json:"eligible"`
|
||||||
|
Joined bool `json:"joined"`
|
||||||
|
IneligibleReason string `json:"ineligible_reason,omitempty"`
|
||||||
|
Period agencyOpeningPeriodData `json:"period"`
|
||||||
|
Qualification agencyOpeningQualificationData `json:"qualification"`
|
||||||
|
Rewards []agencyOpeningRewardData `json:"rewards"`
|
||||||
|
RewardTiers []agencyOpeningRewardData `json:"reward_tiers"`
|
||||||
|
Record agencyOpeningRecordData `json:"record"`
|
||||||
|
Application *agencyOpeningApplicationData `json:"application,omitempty"`
|
||||||
|
Leaderboard []agencyOpeningApplicationData `json:"leaderboard,omitempty"`
|
||||||
|
Rules []string `json:"rules,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyOpeningPeriodData struct {
|
||||||
|
CycleID string `json:"cycle_id,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
StartMS int64 `json:"start_ms,omitempty"`
|
||||||
|
EndMS int64 `json:"end_ms,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyOpeningQualificationData struct {
|
||||||
|
Identity string `json:"agency_identity"`
|
||||||
|
AgencyID int64 `json:"agency_id,omitempty"`
|
||||||
|
AgencyName string `json:"agency_name,omitempty"`
|
||||||
|
HostCount int32 `json:"host_count"`
|
||||||
|
HostRequirement int32 `json:"host_requirement"`
|
||||||
|
OpeningStatus string `json:"opening_status"`
|
||||||
|
Eligible bool `json:"eligible"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyOpeningRewardData struct {
|
||||||
|
RankNo int32 `json:"rank_no"`
|
||||||
|
TierNo int32 `json:"tier_no"`
|
||||||
|
ThresholdCoinSpent int64 `json:"threshold_coin_spent"`
|
||||||
|
OpeningRevenue string `json:"opening_revenue"`
|
||||||
|
RewardCoins int64 `json:"reward_coins"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyOpeningRecordData struct {
|
||||||
|
GuildName string `json:"guild_name,omitempty"`
|
||||||
|
CreatedAt string `json:"created_at,omitempty"`
|
||||||
|
OpeningRevenue string `json:"opening_revenue,omitempty"`
|
||||||
|
Reward int64 `json:"reward"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyOpeningApplicationData struct {
|
||||||
|
ApplicationID string `json:"application_id"`
|
||||||
|
CycleID string `json:"cycle_id"`
|
||||||
|
AgencyID int64 `json:"agency_id"`
|
||||||
|
AgencyOwnerUserID int64 `json:"agency_owner_user_id"`
|
||||||
|
AgencyName string `json:"agency_name"`
|
||||||
|
HostCount int32 `json:"host_count"`
|
||||||
|
AgencyCreatedAtMS int64 `json:"agency_created_at_ms"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
ScoreCoinAmount int64 `json:"score_coin_amount"`
|
||||||
|
RewardRankNo int32 `json:"reward_rank_no"`
|
||||||
|
RewardThresholdCoin int64 `json:"reward_threshold_coin_spent"`
|
||||||
|
RewardCoinAmount int64 `json:"reward_coin_amount"`
|
||||||
|
WalletCommandID string `json:"wallet_command_id,omitempty"`
|
||||||
|
WalletTransactionID string `json:"wallet_transaction_id,omitempty"`
|
||||||
|
FailureReason string `json:"failure_reason,omitempty"`
|
||||||
|
AppliedAtMS int64 `json:"applied_at_ms"`
|
||||||
|
GrantedAtMS int64 `json:"granted_at_ms,omitempty"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAgencyOpeningStatus returns the H5 activity panel from activity-service facts.
|
||||||
|
func (h *Handler) getAgencyOpeningStatus(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if h.agencyOpening == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
|
resp, err := h.agencyOpening.GetAgencyOpeningStatus(request.Context(), &activityv1.GetAgencyOpeningStatusRequest{
|
||||||
|
Meta: httpkit.ActivityMeta(request),
|
||||||
|
UserId: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, agencyOpeningStatusFromProto(resp, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyAgencyOpening only forwards the user action command_id;资格、周期和重复申请都由 activity-service 判定。
|
||||||
|
func (h *Handler) applyAgencyOpening(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if h.agencyOpening == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body agencyOpeningApplyBody
|
||||||
|
if err := httpkit.DecodeJSON(request.Body, &body); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidJSON, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commandID := strings.TrimSpace(body.CommandID)
|
||||||
|
if commandID == "" {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "command_id is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
|
applyResp, err := h.agencyOpening.ApplyAgencyOpening(request.Context(), &activityv1.ApplyAgencyOpeningRequest{
|
||||||
|
Meta: httpkit.ActivityMeta(request),
|
||||||
|
UserId: userID,
|
||||||
|
CommandId: commandID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
statusResp, err := h.agencyOpening.GetAgencyOpeningStatus(request.Context(), &activityv1.GetAgencyOpeningStatusRequest{
|
||||||
|
Meta: httpkit.ActivityMeta(request),
|
||||||
|
UserId: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
|
"created": applyResp.GetCreated(),
|
||||||
|
"joined": true,
|
||||||
|
"application": agencyOpeningApplicationFromProto(applyResp.GetApplication()),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := agencyOpeningStatusFromProto(statusResp, applyResp.GetApplication())
|
||||||
|
data.Joined = true
|
||||||
|
httpkit.WriteOK(writer, request, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// listAgencyOpeningLeaderboard exposes the current or selected cycle榜单 for admin/H5 diagnostics.
|
||||||
|
func (h *Handler) listAgencyOpeningLeaderboard(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if h.agencyOpening == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.agencyOpening.ListAgencyOpeningLeaderboard(request.Context(), &activityv1.ListAgencyOpeningApplicationsRequest{
|
||||||
|
Meta: httpkit.ActivityMeta(request),
|
||||||
|
CycleId: strings.TrimSpace(request.URL.Query().Get("cycle_id")),
|
||||||
|
Status: strings.TrimSpace(request.URL.Query().Get("status")),
|
||||||
|
Page: int32(queryInt(request, "page", 1)),
|
||||||
|
PageSize: int32(queryInt(request, "page_size", 50)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]agencyOpeningApplicationData, 0, len(resp.GetApplications()))
|
||||||
|
for _, item := range resp.GetApplications() {
|
||||||
|
items = append(items, agencyOpeningApplicationFromProto(item))
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningStatusFromProto(item *activityv1.GetAgencyOpeningStatusResponse, applicationOverride *activityv1.AgencyOpeningApplication) agencyOpeningStatusData {
|
||||||
|
if item == nil {
|
||||||
|
return agencyOpeningStatusData{Rewards: []agencyOpeningRewardData{}, RewardTiers: []agencyOpeningRewardData{}, Leaderboard: []agencyOpeningApplicationData{}}
|
||||||
|
}
|
||||||
|
application := item.GetApplication()
|
||||||
|
if applicationOverride != nil {
|
||||||
|
application = applicationOverride
|
||||||
|
}
|
||||||
|
rewards := agencyOpeningRewardsFromProto(item.GetCycle().GetRewards())
|
||||||
|
record := agencyOpeningRecordFromProto(application, item.GetAgency())
|
||||||
|
return agencyOpeningStatusData{
|
||||||
|
ServerTimeMS: item.GetServerTimeMs(),
|
||||||
|
Enabled: item.GetEnabled(),
|
||||||
|
Eligible: item.GetEligible(),
|
||||||
|
Joined: item.GetJoined(),
|
||||||
|
IneligibleReason: item.GetIneligibleReason(),
|
||||||
|
Period: agencyOpeningPeriodData{
|
||||||
|
CycleID: item.GetCycle().GetCycleId(),
|
||||||
|
Title: item.GetCycle().GetTitle(),
|
||||||
|
Status: item.GetCycle().GetStatus(),
|
||||||
|
StartMS: item.GetCycle().GetStartMs(),
|
||||||
|
EndMS: item.GetCycle().GetEndMs(),
|
||||||
|
},
|
||||||
|
Qualification: agencyOpeningQualificationFromProto(item, application),
|
||||||
|
Rewards: rewards,
|
||||||
|
RewardTiers: rewards,
|
||||||
|
Record: record,
|
||||||
|
Application: agencyOpeningApplicationPointerFromProto(application),
|
||||||
|
Rules: agencyOpeningRules(item.GetCycle()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningQualificationFromProto(status *activityv1.GetAgencyOpeningStatusResponse, application *activityv1.AgencyOpeningApplication) agencyOpeningQualificationData {
|
||||||
|
agency := status.GetAgency()
|
||||||
|
hostCount := agency.GetHostCount()
|
||||||
|
agencyID := agency.GetAgencyId()
|
||||||
|
agencyName := agency.GetName()
|
||||||
|
if application != nil {
|
||||||
|
if hostCount == 0 {
|
||||||
|
hostCount = application.GetHostCount()
|
||||||
|
}
|
||||||
|
if agencyID == 0 {
|
||||||
|
agencyID = application.GetAgencyId()
|
||||||
|
}
|
||||||
|
if agencyName == "" {
|
||||||
|
agencyName = application.GetAgencyName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
openingStatus := "Not Joined"
|
||||||
|
if status.GetJoined() {
|
||||||
|
openingStatus = "Joined"
|
||||||
|
}
|
||||||
|
if !status.GetEnabled() {
|
||||||
|
openingStatus = "Disabled"
|
||||||
|
}
|
||||||
|
return agencyOpeningQualificationData{
|
||||||
|
Identity: "Agency",
|
||||||
|
AgencyID: agencyID,
|
||||||
|
AgencyName: agencyName,
|
||||||
|
HostCount: hostCount,
|
||||||
|
HostRequirement: status.GetCycle().GetMinHostCount(),
|
||||||
|
OpeningStatus: openingStatus,
|
||||||
|
Eligible: status.GetEligible() && !status.GetJoined(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningRewardsFromProto(items []*activityv1.AgencyOpeningReward) []agencyOpeningRewardData {
|
||||||
|
rewards := make([]agencyOpeningRewardData, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
rewards = append(rewards, agencyOpeningRewardData{
|
||||||
|
RankNo: item.GetRankNo(),
|
||||||
|
TierNo: item.GetRankNo(),
|
||||||
|
ThresholdCoinSpent: item.GetThresholdCoinSpent(),
|
||||||
|
OpeningRevenue: formatAgencyOpeningCoinLabel(item.GetThresholdCoinSpent()),
|
||||||
|
RewardCoins: item.GetRewardCoinAmount(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rewards
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningRecordFromProto(application *activityv1.AgencyOpeningApplication, agency *activityv1.AgencyOpeningAgencySnapshot) agencyOpeningRecordData {
|
||||||
|
if application == nil {
|
||||||
|
return agencyOpeningRecordData{
|
||||||
|
GuildName: agency.GetName(),
|
||||||
|
CreatedAt: agencyOpeningDate(agency.GetAgencyCreatedAtMs()),
|
||||||
|
OpeningRevenue: "0",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return agencyOpeningRecordData{
|
||||||
|
GuildName: application.GetAgencyName(),
|
||||||
|
CreatedAt: agencyOpeningDate(application.GetAgencyCreatedAtMs()),
|
||||||
|
OpeningRevenue: fmt.Sprintf("%d", application.GetScoreCoinAmount()),
|
||||||
|
Reward: application.GetRewardCoinAmount(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningApplicationPointerFromProto(item *activityv1.AgencyOpeningApplication) *agencyOpeningApplicationData {
|
||||||
|
if item == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
converted := agencyOpeningApplicationFromProto(item)
|
||||||
|
return &converted
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningApplicationFromProto(item *activityv1.AgencyOpeningApplication) agencyOpeningApplicationData {
|
||||||
|
if item == nil {
|
||||||
|
return agencyOpeningApplicationData{}
|
||||||
|
}
|
||||||
|
return agencyOpeningApplicationData{
|
||||||
|
ApplicationID: item.GetApplicationId(),
|
||||||
|
CycleID: item.GetCycleId(),
|
||||||
|
AgencyID: item.GetAgencyId(),
|
||||||
|
AgencyOwnerUserID: item.GetAgencyOwnerUserId(),
|
||||||
|
AgencyName: item.GetAgencyName(),
|
||||||
|
HostCount: item.GetHostCount(),
|
||||||
|
AgencyCreatedAtMS: item.GetAgencyCreatedAtMs(),
|
||||||
|
Status: item.GetStatus(),
|
||||||
|
ScoreCoinAmount: item.GetScoreCoinAmount(),
|
||||||
|
RewardRankNo: item.GetRewardRankNo(),
|
||||||
|
RewardThresholdCoin: item.GetRewardThresholdCoinSpent(),
|
||||||
|
RewardCoinAmount: item.GetRewardCoinAmount(),
|
||||||
|
WalletCommandID: item.GetWalletCommandId(),
|
||||||
|
WalletTransactionID: item.GetWalletTransactionId(),
|
||||||
|
FailureReason: item.GetFailureReason(),
|
||||||
|
AppliedAtMS: item.GetAppliedAtMs(),
|
||||||
|
GrantedAtMS: item.GetGrantedAtMs(),
|
||||||
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAgencyOpeningCoinLabel(value int64) string {
|
||||||
|
if value <= 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
if value%1_000_000 == 0 {
|
||||||
|
return fmt.Sprintf("%dM", value/1_000_000)
|
||||||
|
}
|
||||||
|
if value%1_000 == 0 {
|
||||||
|
return fmt.Sprintf("%dK", value/1_000)
|
||||||
|
}
|
||||||
|
return strconv.FormatInt(value, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningRules(cycle *activityv1.AgencyOpeningCycle) []string {
|
||||||
|
if cycle == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{
|
||||||
|
fmt.Sprintf("Agency must have more than %d hosts before applying.", cycle.GetMinHostCount()),
|
||||||
|
"Opening revenue comes from the applicant user's room gift COIN revenue during the configured period.",
|
||||||
|
"Reward tier is matched by opening revenue after the period ends; it is not based on Top ranking.",
|
||||||
|
"Rewards are granted automatically after the period ends.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func agencyOpeningDate(ms int64) string {
|
||||||
|
if ms <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return time.Unix(ms/1000, (ms%1000)*int64(time.Millisecond)).UTC().Format("2006-01-02")
|
||||||
|
}
|
||||||
@ -0,0 +1,185 @@
|
|||||||
|
package activityapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cpWeeklyRankStatusData struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Period cpWeeklyRankPeriodData `json:"period"`
|
||||||
|
Rewards []cpWeeklyRankRewardData `json:"rewards"`
|
||||||
|
Entries []cpWeeklyRankEntryData `json:"entries"`
|
||||||
|
ServerTimeMS int64 `json:"server_time_ms"`
|
||||||
|
RelationType string `json:"relation_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cpWeeklyRankPeriodData struct {
|
||||||
|
StartMS int64 `json:"start_ms"`
|
||||||
|
EndMS int64 `json:"end_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cpWeeklyRankRewardData struct {
|
||||||
|
RankNo int32 `json:"rank_no"`
|
||||||
|
ResourceGroupID int64 `json:"resource_group_id"`
|
||||||
|
ResourceGroup *checkinResourceGroupData `json:"resource_group,omitempty"`
|
||||||
|
Items []cpWeeklyRankRewardItemData `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cpWeeklyRankRewardItemData struct {
|
||||||
|
ResourceType string `json:"resource_type"`
|
||||||
|
DurationDays int64 `json:"duration_days"`
|
||||||
|
DurationMS int64 `json:"duration_ms"`
|
||||||
|
Quantity int64 `json:"quantity"`
|
||||||
|
CoverURL string `json:"cover_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cpWeeklyRankEntryData struct {
|
||||||
|
Rank int64 `json:"rank"`
|
||||||
|
RelationshipID string `json:"relationship_id"`
|
||||||
|
RelationType string `json:"relation_type"`
|
||||||
|
Score int64 `json:"score"`
|
||||||
|
UserA cpWeeklyRankUserData `json:"user_a"`
|
||||||
|
UserB cpWeeklyRankUserData `json:"user_b"`
|
||||||
|
FormedAtMS int64 `json:"formed_at_ms"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
FirstScoredAtMS int64 `json:"first_scored_at_ms"`
|
||||||
|
LastScoredAtMS int64 `json:"last_scored_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cpWeeklyRankUserData struct {
|
||||||
|
UserID int64 `json:"user_id,string"`
|
||||||
|
DisplayUserID string `json:"display_user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) getCPWeeklyRankStatus(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if h.cpWeeklyRank == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.cpWeeklyRank.GetCPWeeklyRankStatus(request.Context(), &activityv1.GetCPWeeklyRankStatusRequest{
|
||||||
|
Meta: httpkit.ActivityMeta(request),
|
||||||
|
Limit: int32(cpWeeklyRankLimit(request)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
groupIDs := cpWeeklyRankRewardGroupIDs(resp.GetConfig())
|
||||||
|
groups, ok := h.resolveCheckinResourceGroups(writer, request, groupIDs)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, cpWeeklyRankStatusFromProto(resp, groups))
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankStatusFromProto(resp *activityv1.GetCPWeeklyRankStatusResponse, groups map[int64]checkinResourceGroupData) cpWeeklyRankStatusData {
|
||||||
|
config := resp.GetConfig()
|
||||||
|
return cpWeeklyRankStatusData{
|
||||||
|
Enabled: config.GetEnabled(),
|
||||||
|
RelationType: config.GetRelationType(),
|
||||||
|
Period: cpWeeklyRankPeriodData{StartMS: resp.GetPeriodStartMs(), EndMS: resp.GetPeriodEndMs()},
|
||||||
|
Rewards: cpWeeklyRankRewardsFromProto(config.GetRewards(), groups),
|
||||||
|
Entries: cpWeeklyRankEntriesFromProto(resp.GetEntries()),
|
||||||
|
ServerTimeMS: resp.GetServerTimeMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankRewardsFromProto(items []*activityv1.CPWeeklyRankReward, groups map[int64]checkinResourceGroupData) []cpWeeklyRankRewardData {
|
||||||
|
rewards := make([]cpWeeklyRankRewardData, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
reward := cpWeeklyRankRewardData{
|
||||||
|
RankNo: item.GetRankNo(),
|
||||||
|
ResourceGroupID: item.GetResourceGroupId(),
|
||||||
|
Items: []cpWeeklyRankRewardItemData{},
|
||||||
|
}
|
||||||
|
if group, ok := groups[item.GetResourceGroupId()]; ok {
|
||||||
|
reward.ResourceGroup = &group
|
||||||
|
reward.Items = cpWeeklyRankRewardItemsFromGroup(group)
|
||||||
|
}
|
||||||
|
rewards = append(rewards, reward)
|
||||||
|
}
|
||||||
|
return rewards
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankRewardItemsFromGroup(group checkinResourceGroupData) []cpWeeklyRankRewardItemData {
|
||||||
|
items := make([]cpWeeklyRankRewardItemData, 0, len(group.Items))
|
||||||
|
for _, item := range group.Items {
|
||||||
|
reward := cpWeeklyRankRewardItemData{
|
||||||
|
DurationMS: item.DurationMS,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
}
|
||||||
|
if item.DurationMS > 0 {
|
||||||
|
reward.DurationDays = item.DurationMS / (24 * 60 * 60 * 1000)
|
||||||
|
}
|
||||||
|
if item.Resource != nil {
|
||||||
|
reward.ResourceType = item.Resource.ResourceType
|
||||||
|
reward.CoverURL = firstRechargeFirstNonEmpty(item.Resource.PreviewURL, item.Resource.AssetURL, item.Resource.AnimationURL)
|
||||||
|
} else {
|
||||||
|
reward.ResourceType = firstRechargeWalletAssetType(item.WalletAssetType)
|
||||||
|
reward.Quantity = firstRechargePositiveOr(item.WalletAssetAmount, item.Quantity)
|
||||||
|
}
|
||||||
|
items = append(items, reward)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankEntriesFromProto(items []*activityv1.CPWeeklyRankEntry) []cpWeeklyRankEntryData {
|
||||||
|
entries := make([]cpWeeklyRankEntryData, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
entries = append(entries, cpWeeklyRankEntryData{
|
||||||
|
Rank: item.GetRank(),
|
||||||
|
RelationshipID: item.GetRelationshipId(),
|
||||||
|
RelationType: item.GetRelationType(),
|
||||||
|
Score: item.GetScore(),
|
||||||
|
UserA: cpWeeklyRankUserFromProto(item.GetUserA()),
|
||||||
|
UserB: cpWeeklyRankUserFromProto(item.GetUserB()),
|
||||||
|
FormedAtMS: item.GetFormedAtMs(),
|
||||||
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
|
FirstScoredAtMS: item.GetFirstScoredAtMs(),
|
||||||
|
LastScoredAtMS: item.GetLastScoredAtMs(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankUserFromProto(item *activityv1.CPWeeklyRankUser) cpWeeklyRankUserData {
|
||||||
|
if item == nil {
|
||||||
|
return cpWeeklyRankUserData{}
|
||||||
|
}
|
||||||
|
return cpWeeklyRankUserData{
|
||||||
|
UserID: item.GetUserId(),
|
||||||
|
DisplayUserID: item.GetDisplayUserId(),
|
||||||
|
Username: item.GetUsername(),
|
||||||
|
Avatar: item.GetAvatar(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankRewardGroupIDs(config *activityv1.CPWeeklyRankConfig) []int64 {
|
||||||
|
if config == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ids := make([]int64, 0, len(config.GetRewards()))
|
||||||
|
for _, reward := range config.GetRewards() {
|
||||||
|
if reward.GetResourceGroupId() > 0 {
|
||||||
|
ids = append(ids, reward.GetResourceGroupId())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpWeeklyRankLimit(request *http.Request) int {
|
||||||
|
value, _ := strconv.Atoi(request.URL.Query().Get("limit"))
|
||||||
|
if value <= 0 {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
if value > 20 {
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
@ -25,6 +25,8 @@ type Handler struct {
|
|||||||
luckyGift client.LuckyGiftClient
|
luckyGift client.LuckyGiftClient
|
||||||
roomTurnoverReward client.RoomTurnoverRewardClient
|
roomTurnoverReward client.RoomTurnoverRewardClient
|
||||||
weeklyStar client.WeeklyStarClient
|
weeklyStar client.WeeklyStarClient
|
||||||
|
cpWeeklyRank client.CPWeeklyRankClient
|
||||||
|
agencyOpening client.AgencyOpeningClient
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -43,6 +45,8 @@ type Config struct {
|
|||||||
LuckyGift client.LuckyGiftClient
|
LuckyGift client.LuckyGiftClient
|
||||||
RoomTurnoverReward client.RoomTurnoverRewardClient
|
RoomTurnoverReward client.RoomTurnoverRewardClient
|
||||||
WeeklyStar client.WeeklyStarClient
|
WeeklyStar client.WeeklyStarClient
|
||||||
|
CPWeeklyRank client.CPWeeklyRankClient
|
||||||
|
AgencyOpening client.AgencyOpeningClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(config Config) *Handler {
|
func New(config Config) *Handler {
|
||||||
@ -62,6 +66,8 @@ func New(config Config) *Handler {
|
|||||||
luckyGift: config.LuckyGift,
|
luckyGift: config.LuckyGift,
|
||||||
roomTurnoverReward: config.RoomTurnoverReward,
|
roomTurnoverReward: config.RoomTurnoverReward,
|
||||||
weeklyStar: config.WeeklyStar,
|
weeklyStar: config.WeeklyStar,
|
||||||
|
cpWeeklyRank: config.CPWeeklyRank,
|
||||||
|
agencyOpening: config.AgencyOpening,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +89,10 @@ func (h *Handler) TaskHandlers() httproutes.TaskHandlers {
|
|||||||
GetWeeklyStarCurrent: h.getWeeklyStarCurrent,
|
GetWeeklyStarCurrent: h.getWeeklyStarCurrent,
|
||||||
ListWeeklyStarLeaderboard: h.listWeeklyStarLeaderboard,
|
ListWeeklyStarLeaderboard: h.listWeeklyStarLeaderboard,
|
||||||
ListWeeklyStarHistory: h.listWeeklyStarHistory,
|
ListWeeklyStarHistory: h.listWeeklyStarHistory,
|
||||||
|
GetCPWeeklyRankStatus: h.getCPWeeklyRankStatus,
|
||||||
|
GetAgencyOpeningStatus: h.getAgencyOpeningStatus,
|
||||||
|
ApplyAgencyOpening: h.applyAgencyOpening,
|
||||||
|
ListAgencyOpeningLeaderboard: h.listAgencyOpeningLeaderboard,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -133,6 +133,10 @@ func (f *fakeInviteActivityUserProfileClient) UpdateUserProfile(context.Context,
|
|||||||
return &userv1.UpdateUserProfileResponse{}, nil
|
return &userv1.UpdateUserProfileResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeInviteActivityUserProfileClient) UpdateUserProfileBackground(context.Context, *userv1.UpdateUserProfileBackgroundRequest) (*userv1.UpdateUserProfileBackgroundResponse, error) {
|
||||||
|
return &userv1.UpdateUserProfileBackgroundResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeInviteActivityUserProfileClient) ChangeUserCountry(context.Context, *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) {
|
func (f *fakeInviteActivityUserProfileClient) ChangeUserCountry(context.Context, *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) {
|
||||||
return &userv1.ChangeUserCountryResponse{}, nil
|
return &userv1.ChangeUserCountryResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,6 +49,8 @@ type Handler struct {
|
|||||||
luckyGift client.LuckyGiftClient
|
luckyGift client.LuckyGiftClient
|
||||||
roomTurnoverReward client.RoomTurnoverRewardClient
|
roomTurnoverReward client.RoomTurnoverRewardClient
|
||||||
weeklyStar client.WeeklyStarClient
|
weeklyStar client.WeeklyStarClient
|
||||||
|
cpWeeklyRank client.CPWeeklyRankClient
|
||||||
|
agencyOpening client.AgencyOpeningClient
|
||||||
broadcastClient client.BroadcastClient
|
broadcastClient client.BroadcastClient
|
||||||
gameClient client.GameClient
|
gameClient client.GameClient
|
||||||
statisticsClient client.StatisticsClient
|
statisticsClient client.StatisticsClient
|
||||||
@ -265,6 +267,16 @@ func (h *Handler) SetWeeklyStarClient(weeklyStar client.WeeklyStarClient) {
|
|||||||
h.weeklyStar = weeklyStar
|
h.weeklyStar = weeklyStar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetCPWeeklyRankClient 注入 activity-service CP 周榜活动 client。
|
||||||
|
func (h *Handler) SetCPWeeklyRankClient(cpWeeklyRank client.CPWeeklyRankClient) {
|
||||||
|
h.cpWeeklyRank = cpWeeklyRank
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAgencyOpeningClient 注入 activity-service 代理开业活动 client。
|
||||||
|
func (h *Handler) SetAgencyOpeningClient(agencyOpening client.AgencyOpeningClient) {
|
||||||
|
h.agencyOpening = agencyOpening
|
||||||
|
}
|
||||||
|
|
||||||
// SetBroadcastClient 注入 activity-service 播报群成员关系 client。
|
// SetBroadcastClient 注入 activity-service 播报群成员关系 client。
|
||||||
func (h *Handler) SetBroadcastClient(broadcastClient client.BroadcastClient) {
|
func (h *Handler) SetBroadcastClient(broadcastClient client.BroadcastClient) {
|
||||||
h.broadcastClient = broadcastClient
|
h.broadcastClient = broadcastClient
|
||||||
|
|||||||
@ -97,6 +97,7 @@ type UserHandlers struct {
|
|||||||
ProcessMyRoleInvitation http.HandlerFunc
|
ProcessMyRoleInvitation http.HandlerFunc
|
||||||
CompleteMyOnboarding http.HandlerFunc
|
CompleteMyOnboarding http.HandlerFunc
|
||||||
UpdateMyProfile http.HandlerFunc
|
UpdateMyProfile http.HandlerFunc
|
||||||
|
UpdateMyProfileBackground http.HandlerFunc
|
||||||
ChangeMyCountry http.HandlerFunc
|
ChangeMyCountry http.HandlerFunc
|
||||||
ChangeMyDisplayUserID http.HandlerFunc
|
ChangeMyDisplayUserID http.HandlerFunc
|
||||||
ApplyMyPrettyDisplayUserID http.HandlerFunc
|
ApplyMyPrettyDisplayUserID http.HandlerFunc
|
||||||
@ -201,6 +202,10 @@ type TaskHandlers struct {
|
|||||||
GetWeeklyStarCurrent http.HandlerFunc
|
GetWeeklyStarCurrent http.HandlerFunc
|
||||||
ListWeeklyStarLeaderboard http.HandlerFunc
|
ListWeeklyStarLeaderboard http.HandlerFunc
|
||||||
ListWeeklyStarHistory http.HandlerFunc
|
ListWeeklyStarHistory http.HandlerFunc
|
||||||
|
GetCPWeeklyRankStatus http.HandlerFunc
|
||||||
|
GetAgencyOpeningStatus http.HandlerFunc
|
||||||
|
ApplyAgencyOpening http.HandlerFunc
|
||||||
|
ListAgencyOpeningLeaderboard http.HandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
type LevelHandlers struct {
|
type LevelHandlers struct {
|
||||||
@ -415,6 +420,7 @@ func (r routes) registerUserRoutes() {
|
|||||||
r.profile("/role-invitations/{invitation_id}/process", http.MethodPost, h.ProcessMyRoleInvitation)
|
r.profile("/role-invitations/{invitation_id}/process", http.MethodPost, h.ProcessMyRoleInvitation)
|
||||||
r.auth("/users/me/onboarding/complete", "", h.CompleteMyOnboarding)
|
r.auth("/users/me/onboarding/complete", "", h.CompleteMyOnboarding)
|
||||||
r.profile("/users/me/profile/update", "", h.UpdateMyProfile)
|
r.profile("/users/me/profile/update", "", h.UpdateMyProfile)
|
||||||
|
r.profile("/users/me/profile-bg-img", http.MethodPost, h.UpdateMyProfileBackground)
|
||||||
r.profile("/users/me/country/change", "", h.ChangeMyCountry)
|
r.profile("/users/me/country/change", "", h.ChangeMyCountry)
|
||||||
r.auth("/users/me/display-id/change", "", h.ChangeMyDisplayUserID)
|
r.auth("/users/me/display-id/change", "", h.ChangeMyDisplayUserID)
|
||||||
r.auth("/users/me/display-id/pretty/apply", "", h.ApplyMyPrettyDisplayUserID)
|
r.auth("/users/me/display-id/pretty/apply", "", h.ApplyMyPrettyDisplayUserID)
|
||||||
@ -518,6 +524,10 @@ func (r routes) registerTaskRoutes() {
|
|||||||
r.profile("/activities/weekly-star/current", http.MethodGet, h.GetWeeklyStarCurrent)
|
r.profile("/activities/weekly-star/current", http.MethodGet, h.GetWeeklyStarCurrent)
|
||||||
r.profile("/activities/weekly-star/leaderboard", http.MethodGet, h.ListWeeklyStarLeaderboard)
|
r.profile("/activities/weekly-star/leaderboard", http.MethodGet, h.ListWeeklyStarLeaderboard)
|
||||||
r.profile("/activities/weekly-star/history", http.MethodGet, h.ListWeeklyStarHistory)
|
r.profile("/activities/weekly-star/history", http.MethodGet, h.ListWeeklyStarHistory)
|
||||||
|
r.profile("/activities/cp-weekly-rank", http.MethodGet, h.GetCPWeeklyRankStatus)
|
||||||
|
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)
|
||||||
// App 侧接口保持稳定;内部仍按 pool_id 走当前 v2 规则、抽奖和返奖实现。
|
// App 侧接口保持稳定;内部仍按 pool_id 走当前 v2 规则、抽奖和返奖实现。
|
||||||
r.profile("/activities/lucky-gifts/check", http.MethodPost, h.CheckLuckyGift)
|
r.profile("/activities/lucky-gifts/check", http.MethodPost, h.CheckLuckyGift)
|
||||||
r.profile("/tasks/tabs", http.MethodGet, h.ListTaskTabs)
|
r.profile("/tasks/tabs", http.MethodGet, h.ListTaskTabs)
|
||||||
|
|||||||
@ -17,6 +17,16 @@ import (
|
|||||||
func TestGetMyOverviewComposesProfileWalletVIPStatsRolesAndEntries(t *testing.T) {
|
func TestGetMyOverviewComposesProfileWalletVIPStatsRolesAndEntries(t *testing.T) {
|
||||||
profileClient := &fakeUserProfileClient{
|
profileClient := &fakeUserProfileClient{
|
||||||
regionID: 10,
|
regionID: 10,
|
||||||
|
usersByID: map[int64]*userv1.User{42: {
|
||||||
|
UserId: 42,
|
||||||
|
DisplayUserId: "100001",
|
||||||
|
Username: "hy",
|
||||||
|
Gender: "female",
|
||||||
|
RegionId: 10,
|
||||||
|
ProfileCompleted: true,
|
||||||
|
OnboardingStatus: "completed",
|
||||||
|
ProfileBgImg: "https://cdn.example/overview-profile-bg.png",
|
||||||
|
}},
|
||||||
statsResp: &userv1.GetMyProfileStatsResponse{Stats: &userv1.UserProfileStats{
|
statsResp: &userv1.GetMyProfileStatsResponse{Stats: &userv1.UserProfileStats{
|
||||||
UserId: 42,
|
UserId: 42,
|
||||||
VisitorsCount: 1234,
|
VisitorsCount: 1234,
|
||||||
@ -74,7 +84,7 @@ func TestGetMyOverviewComposesProfileWalletVIPStatsRolesAndEntries(t *testing.T)
|
|||||||
}
|
}
|
||||||
|
|
||||||
profile := data["profile"].(map[string]any)
|
profile := data["profile"].(map[string]any)
|
||||||
if profile["user_id"] != "42" || profile["username"] != "hy" || profile["region_id"] != float64(10) {
|
if profile["user_id"] != "42" || profile["username"] != "hy" || profile["region_id"] != float64(10) || profile["profile_bg_img"] != "https://cdn.example/overview-profile-bg.png" {
|
||||||
t.Fatalf("profile data mismatch: %+v", profile)
|
t.Fatalf("profile data mismatch: %+v", profile)
|
||||||
}
|
}
|
||||||
stats := data["profile_stats"].(map[string]any)
|
stats := data["profile_stats"].(map[string]any)
|
||||||
|
|||||||
@ -310,6 +310,7 @@ type fakeUserProfileClient struct {
|
|||||||
getRequests []*userv1.GetUserRequest
|
getRequests []*userv1.GetUserRequest
|
||||||
lastComplete *userv1.CompleteOnboardingRequest
|
lastComplete *userv1.CompleteOnboardingRequest
|
||||||
lastUpdate *userv1.UpdateUserProfileRequest
|
lastUpdate *userv1.UpdateUserProfileRequest
|
||||||
|
lastBackground *userv1.UpdateUserProfileBackgroundRequest
|
||||||
lastCountry *userv1.ChangeUserCountryRequest
|
lastCountry *userv1.ChangeUserCountryRequest
|
||||||
lastCreateBlock *userv1.CreateManagerUserBlockRequest
|
lastCreateBlock *userv1.CreateManagerUserBlockRequest
|
||||||
lastListBlocks *userv1.ListManagerUserBlocksRequest
|
lastListBlocks *userv1.ListManagerUserBlocksRequest
|
||||||
@ -830,6 +831,15 @@ func (f *fakeUserProfileClient) UpdateUserProfile(_ context.Context, req *userv1
|
|||||||
}}, nil
|
}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeUserProfileClient) UpdateUserProfileBackground(_ context.Context, req *userv1.UpdateUserProfileBackgroundRequest) (*userv1.UpdateUserProfileBackgroundResponse, error) {
|
||||||
|
f.lastBackground = req
|
||||||
|
return &userv1.UpdateUserProfileBackgroundResponse{User: &userv1.User{
|
||||||
|
UserId: req.GetUserId(),
|
||||||
|
ProfileBgImg: req.GetProfileBgImg(),
|
||||||
|
UpdatedAtMs: 2100,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeUserProfileClient) ChangeUserCountry(_ context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) {
|
func (f *fakeUserProfileClient) ChangeUserCountry(_ context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) {
|
||||||
f.lastCountry = req
|
f.lastCountry = req
|
||||||
if f.countryErr != nil {
|
if f.countryErr != nil {
|
||||||
@ -5391,7 +5401,7 @@ func TestSetPasswordRequiresTokenAndPropagatesActorUserID(t *testing.T) {
|
|||||||
func TestUpdateProfileUsesAuthenticatedUserID(t *testing.T) {
|
func TestUpdateProfileUsesAuthenticatedUserID(t *testing.T) {
|
||||||
profileClient := &fakeUserProfileClient{}
|
profileClient := &fakeUserProfileClient{}
|
||||||
router := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient).Routes(auth.NewVerifier("secret"))
|
router := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient).Routes(auth.NewVerifier("secret"))
|
||||||
body := []byte(`{"username":"hy","avatar":"https://cdn.example/a.png","gender":"female","birth":"2000-01-02"}`)
|
body := []byte(`{"username":"hy","avatar":"https://cdn.example/a.png","gender":"female","birth":"2000-01-02","profile_bg_img":"https://cdn.example/bg.png","user_id":99}`)
|
||||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/users/me/profile/update", bytes.NewReader(body))
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/users/me/profile/update", bytes.NewReader(body))
|
||||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
request.Header.Set("X-Request-ID", "req-profile")
|
request.Header.Set("X-Request-ID", "req-profile")
|
||||||
@ -5408,11 +5418,53 @@ func TestUpdateProfileUsesAuthenticatedUserID(t *testing.T) {
|
|||||||
if profileClient.lastUpdate.GetMeta().GetRequestId() != assertGeneratedRequestID(t, recorder, "req-profile") || valueOfString(profileClient.lastUpdate.Username) != "hy" || valueOfString(profileClient.lastUpdate.Gender) != "female" || valueOfString(profileClient.lastUpdate.Birth) != "2000-01-02" {
|
if profileClient.lastUpdate.GetMeta().GetRequestId() != assertGeneratedRequestID(t, recorder, "req-profile") || valueOfString(profileClient.lastUpdate.Username) != "hy" || valueOfString(profileClient.lastUpdate.Gender) != "female" || valueOfString(profileClient.lastUpdate.Birth) != "2000-01-02" {
|
||||||
t.Fatalf("profile update request mismatch: %+v", profileClient.lastUpdate)
|
t.Fatalf("profile update request mismatch: %+v", profileClient.lastUpdate)
|
||||||
}
|
}
|
||||||
|
if profileClient.lastBackground != nil {
|
||||||
|
t.Fatalf("profile update must not modify profile_bg_img: %+v", profileClient.lastBackground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateProfileBackgroundUsesAuthenticatedUserIDAndURL(t *testing.T) {
|
||||||
|
profileClient := &fakeUserProfileClient{}
|
||||||
|
router := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient).Routes(auth.NewVerifier("secret"))
|
||||||
|
body := []byte(`{"url":"https://cdn.example/profile-bg.png","user_id":99}`)
|
||||||
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/users/me/profile-bg-img", bytes.NewReader(body))
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
|
request.Header.Set("X-Request-ID", "req-profile-bg")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if profileClient.lastBackground == nil || profileClient.lastBackground.GetUserId() != 42 {
|
||||||
|
t.Fatalf("authenticated user_id was not propagated to profile background update: %+v", profileClient.lastBackground)
|
||||||
|
}
|
||||||
|
if profileClient.lastBackground.GetMeta().GetRequestId() != assertGeneratedRequestID(t, recorder, "req-profile-bg") || profileClient.lastBackground.GetProfileBgImg() != "https://cdn.example/profile-bg.png" {
|
||||||
|
t.Fatalf("profile background request mismatch: %+v", profileClient.lastBackground)
|
||||||
|
}
|
||||||
|
var response httpkit.ResponseEnvelope
|
||||||
|
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||||
|
t.Fatalf("decode response failed: %v", err)
|
||||||
|
}
|
||||||
|
data, ok := response.Data.(map[string]any)
|
||||||
|
if response.Code != httpkit.CodeOK || !ok || data["profile_bg_img"] != "https://cdn.example/profile-bg.png" {
|
||||||
|
t.Fatalf("profile background response mismatch: %+v", response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetMyProfileUsesAuthenticatedUserID(t *testing.T) {
|
func TestGetMyProfileUsesAuthenticatedUserID(t *testing.T) {
|
||||||
profileClient := &fakeUserProfileClient{}
|
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{42: {
|
||||||
router := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient).Routes(auth.NewVerifier("secret"))
|
UserId: 42,
|
||||||
|
DisplayUserId: "100001",
|
||||||
|
Username: "hy",
|
||||||
|
Gender: "female",
|
||||||
|
ProfileCompleted: true,
|
||||||
|
OnboardingStatus: "completed",
|
||||||
|
ProfileBgImg: "https://cdn.example/profile-bg.png",
|
||||||
|
}}}
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
||||||
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/users/me", nil)
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/users/me", nil)
|
||||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
request.Header.Set("X-Request-ID", "req-profile-get")
|
request.Header.Set("X-Request-ID", "req-profile-get")
|
||||||
@ -5431,7 +5483,7 @@ func TestGetMyProfileUsesAuthenticatedUserID(t *testing.T) {
|
|||||||
t.Fatalf("decode response failed: %v", err)
|
t.Fatalf("decode response failed: %v", err)
|
||||||
}
|
}
|
||||||
data, ok := response.Data.(map[string]any)
|
data, ok := response.Data.(map[string]any)
|
||||||
if response.Code != httpkit.CodeOK || !ok || data["gender"] != "female" {
|
if response.Code != httpkit.CodeOK || !ok || data["gender"] != "female" || data["profile_bg_img"] != "https://cdn.example/profile-bg.png" {
|
||||||
t.Fatalf("profile response mismatch: %+v", response)
|
t.Fatalf("profile response mismatch: %+v", response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7073,6 +7125,57 @@ func TestConfirmGooglePaymentUsesAuthenticatedUserAndProfileRegion(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfirmGooglePaymentAllowsGoogleProductCodeWithoutProductID(t *testing.T) {
|
||||||
|
walletClient := &fakeWalletClient{googleConfirmResp: &walletv1.ConfirmGooglePaymentResponse{
|
||||||
|
PaymentOrderId: "gpay_first_recharge",
|
||||||
|
TransactionId: "wtx_first_recharge",
|
||||||
|
Status: "credited",
|
||||||
|
ProductId: 11,
|
||||||
|
ProductCode: "first_recharge_google_999",
|
||||||
|
CoinAmount: 90000,
|
||||||
|
Balance: &walletv1.AssetBalance{
|
||||||
|
AssetType: "COIN",
|
||||||
|
AvailableAmount: 90000,
|
||||||
|
},
|
||||||
|
ConsumeState: "consumed",
|
||||||
|
}}
|
||||||
|
profileClient := &fakeUserProfileClient{regionID: 2002}
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
||||||
|
handler.SetWalletClient(walletClient)
|
||||||
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
|
body := `{"command_id":"pay-cmd-first-recharge","product_code":"first_recharge_google_999","package_name":"com.hyapp.lalu","purchase_token":"purchase-token","order_id":"GPA.1","purchase_time_ms":1710000000000}`
|
||||||
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/wallet/payments/google/confirm", bytes.NewBufferString(body))
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
request.Header.Set("X-Request-ID", "req-google-confirm-first-recharge")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if walletClient.lastGoogleConfirm == nil ||
|
||||||
|
walletClient.lastGoogleConfirm.GetUserId() != 42 ||
|
||||||
|
walletClient.lastGoogleConfirm.GetRegionId() != 2002 ||
|
||||||
|
walletClient.lastGoogleConfirm.GetProductId() != 0 ||
|
||||||
|
walletClient.lastGoogleConfirm.GetProductCode() != "first_recharge_google_999" ||
|
||||||
|
walletClient.lastGoogleConfirm.GetPackageName() != "com.hyapp.lalu" ||
|
||||||
|
walletClient.lastGoogleConfirm.GetPurchaseToken() != "purchase-token" ||
|
||||||
|
walletClient.lastGoogleConfirm.GetCommandId() != "pay-cmd-first-recharge" ||
|
||||||
|
walletClient.lastGoogleConfirm.GetRequestId() != assertGeneratedRequestID(t, recorder, "req-google-confirm-first-recharge") {
|
||||||
|
t.Fatalf("google first recharge confirm request mismatch: %+v", walletClient.lastGoogleConfirm)
|
||||||
|
}
|
||||||
|
var response httpkit.ResponseEnvelope
|
||||||
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
|
t.Fatalf("decode response failed: %v", err)
|
||||||
|
}
|
||||||
|
data, ok := response.Data.(map[string]any)
|
||||||
|
if response.Code != httpkit.CodeOK || !ok || data["payment_order_id"] != "gpay_first_recharge" || data["consume_state"] != "consumed" || data["coin_amount"] != float64(90000) {
|
||||||
|
t.Fatalf("google first recharge confirm response mismatch: %+v", response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestH5RechargeOptionsUsesTokenCountryAndCoinSellerAudience(t *testing.T) {
|
func TestH5RechargeOptionsUsesTokenCountryAndCoinSellerAudience(t *testing.T) {
|
||||||
walletClient := &fakeWalletClient{h5OptionsResp: &walletv1.H5RechargeOptionsResponse{
|
walletClient := &fakeWalletClient{h5OptionsResp: &walletv1.H5RechargeOptionsResponse{
|
||||||
Products: []*walletv1.RechargeProduct{{
|
Products: []*walletv1.RechargeProduct{{
|
||||||
|
|||||||
@ -84,6 +84,8 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
|||||||
LuckyGift: h.luckyGift,
|
LuckyGift: h.luckyGift,
|
||||||
RoomTurnoverReward: h.roomTurnoverReward,
|
RoomTurnoverReward: h.roomTurnoverReward,
|
||||||
WeeklyStar: h.weeklyStar,
|
WeeklyStar: h.weeklyStar,
|
||||||
|
CPWeeklyRank: h.cpWeeklyRank,
|
||||||
|
AgencyOpening: h.agencyOpening,
|
||||||
})
|
})
|
||||||
gameAPI := gameapi.New(gameapi.Config{
|
gameAPI := gameapi.New(gameapi.Config{
|
||||||
GameClient: h.gameClient,
|
GameClient: h.gameClient,
|
||||||
|
|||||||
@ -126,6 +126,47 @@ func TestFriendApplicationFlowRoutesUseBackendThenIMOwnedReminder(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProfileVisitReturnsTargetProfileBackground(t *testing.T) {
|
||||||
|
socialClient := &fakeUserSocialClient{}
|
||||||
|
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{10002: {
|
||||||
|
UserId: 10002,
|
||||||
|
DisplayUserId: "10002",
|
||||||
|
Username: "target",
|
||||||
|
ProfileBgImg: "https://cdn.example/target-profile-bg.png",
|
||||||
|
}}}
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
||||||
|
handler.SetUserSocialClient(socialClient)
|
||||||
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/users/10002/visit", nil)
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 10001))
|
||||||
|
request.Header.Set("X-Request-ID", "req-visit-profile-bg")
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(response, request)
|
||||||
|
|
||||||
|
if response.Code != http.StatusOK {
|
||||||
|
t.Fatalf("profile visit status = %d body=%s", response.Code, response.Body.String())
|
||||||
|
}
|
||||||
|
if socialClient.lastVisit == nil || socialClient.lastVisit.GetVisitorUserId() != 10001 || socialClient.lastVisit.GetTargetUserId() != 10002 {
|
||||||
|
t.Fatalf("visit request mismatch: %+v", socialClient.lastVisit)
|
||||||
|
}
|
||||||
|
if profileClient.lastGet == nil || profileClient.lastGet.GetUserId() != 10002 {
|
||||||
|
t.Fatalf("target profile request mismatch: %+v", profileClient.lastGet)
|
||||||
|
}
|
||||||
|
var envelope httpkit.ResponseEnvelope
|
||||||
|
if err := json.Unmarshal(response.Body.Bytes(), &envelope); err != nil || envelope.Code != httpkit.CodeOK {
|
||||||
|
t.Fatalf("invalid envelope: %+v err=%v", envelope, err)
|
||||||
|
}
|
||||||
|
data, ok := envelope.Data.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("invalid data: %+v", envelope.Data)
|
||||||
|
}
|
||||||
|
profile, ok := data["target_profile"].(map[string]any)
|
||||||
|
if !ok || profile["user_id"] != "10002" || profile["profile_bg_img"] != "https://cdn.example/target-profile-bg.png" {
|
||||||
|
t.Fatalf("target profile background mismatch: %+v", data["target_profile"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSubmitReportRoutePassesAuthenticatedReporterAndSingleTarget(t *testing.T) {
|
func TestSubmitReportRoutePassesAuthenticatedReporterAndSingleTarget(t *testing.T) {
|
||||||
socialClient := &fakeUserSocialClient{}
|
socialClient := &fakeUserSocialClient{}
|
||||||
handler := NewHandler(&fakeRoomClient{})
|
handler := NewHandler(&fakeRoomClient{})
|
||||||
|
|||||||
@ -81,6 +81,7 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
|||||||
ProcessMyRoleInvitation: h.processMyRoleInvitation,
|
ProcessMyRoleInvitation: h.processMyRoleInvitation,
|
||||||
CompleteMyOnboarding: h.completeMyOnboarding,
|
CompleteMyOnboarding: h.completeMyOnboarding,
|
||||||
UpdateMyProfile: h.updateMyProfile,
|
UpdateMyProfile: h.updateMyProfile,
|
||||||
|
UpdateMyProfileBackground: h.updateMyProfileBackground,
|
||||||
ChangeMyCountry: h.changeMyCountry,
|
ChangeMyCountry: h.changeMyCountry,
|
||||||
ChangeMyDisplayUserID: h.changeMyDisplayUserID,
|
ChangeMyDisplayUserID: h.changeMyDisplayUserID,
|
||||||
ApplyMyPrettyDisplayUserID: h.applyMyPrettyDisplayUserID,
|
ApplyMyPrettyDisplayUserID: h.applyMyPrettyDisplayUserID,
|
||||||
|
|||||||
@ -192,7 +192,18 @@ func (h *Handler) recordProfileVisit(writer http.ResponseWriter, request *http.R
|
|||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
httpkit.WriteOK(writer, request, map[string]any{"recorded": resp.GetRecorded(), "target_stats": overviewStatsFromProto(resp.GetTargetStats())})
|
data := map[string]any{"recorded": resp.GetRecorded(), "target_stats": overviewStatsFromProto(resp.GetTargetStats())}
|
||||||
|
if h.userProfileClient != nil {
|
||||||
|
profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
|
UserId: targetUserID,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
// 访问记录归 social read model,资料卡展示归 user-service 用户主资料;这里仅拼装 App 打开别人资料卡所需的当前快照。
|
||||||
|
data["target_profile"] = profileData(profileResp.GetUser(), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleFollowAction(writer http.ResponseWriter, request *http.Request, followeeUserID int64) {
|
func (h *Handler) handleFollowAction(writer http.ResponseWriter, request *http.Request, followeeUserID int64) {
|
||||||
|
|||||||
@ -62,6 +62,7 @@ type userProfileData struct {
|
|||||||
ProfileCompletedAtMs int64 `json:"profile_completed_at_ms"`
|
ProfileCompletedAtMs int64 `json:"profile_completed_at_ms"`
|
||||||
OnboardingStatus string `json:"onboarding_status"`
|
OnboardingStatus string `json:"onboarding_status"`
|
||||||
UpdatedAtMs int64 `json:"updated_at_ms"`
|
UpdatedAtMs int64 `json:"updated_at_ms"`
|
||||||
|
ProfileBgImg string `json:"profile_bg_img"`
|
||||||
NextCountryChangeAtMs int64 `json:"next_country_change_allowed_at_ms,omitempty"`
|
NextCountryChangeAtMs int64 `json:"next_country_change_allowed_at_ms,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,7 +324,7 @@ func (h *Handler) getMyInviteOverview(writer http.ResponseWriter, request *http.
|
|||||||
httpkit.WriteOK(writer, request, inviteOverviewDataFromProto(resp.GetUser().GetInvite()))
|
httpkit.WriteOK(writer, request, inviteOverviewDataFromProto(resp.GetUser().GetInvite()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateMyProfile 修改当前用户的用户名、头像、性别和生日。
|
// updateMyProfile 修改当前用户的用户名、头像、性别和生日;主页背景图走专用上传接口。
|
||||||
func (h *Handler) updateMyProfile(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) updateMyProfile(writer http.ResponseWriter, request *http.Request) {
|
||||||
var body struct {
|
var body struct {
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
@ -355,6 +356,35 @@ func (h *Handler) updateMyProfile(writer http.ResponseWriter, request *http.Requ
|
|||||||
httpkit.WriteOK(writer, request, profileData(resp.GetUser(), 0))
|
httpkit.WriteOK(writer, request, profileData(resp.GetUser(), 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateMyProfileBackground 修改当前用户个人信息页背景图;客户端先上传图片,本接口只保存 URL。
|
||||||
|
func (h *Handler) updateMyProfileBackground(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
var body struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.userProfileClient == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.userProfileClient.UpdateUserProfileBackground(request.Context(), &userv1.UpdateUserProfileBackgroundRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
|
ProfileBgImg: body.URL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
|
"profile_bg_img": resp.GetUser().GetProfileBgImg(),
|
||||||
|
"profile": profileData(resp.GetUser(), 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// changeMyCountry 单独修改当前用户国家;冷却期和审计由 user-service 保证。
|
// changeMyCountry 单独修改当前用户国家;冷却期和审计由 user-service 保证。
|
||||||
func (h *Handler) changeMyCountry(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) changeMyCountry(writer http.ResponseWriter, request *http.Request) {
|
||||||
var body struct {
|
var body struct {
|
||||||
@ -589,6 +619,7 @@ func profileData(user *userv1.User, nextCountryChangeAtMs int64) userProfileData
|
|||||||
ProfileCompletedAtMs: user.GetProfileCompletedAtMs(),
|
ProfileCompletedAtMs: user.GetProfileCompletedAtMs(),
|
||||||
OnboardingStatus: user.GetOnboardingStatus(),
|
OnboardingStatus: user.GetOnboardingStatus(),
|
||||||
UpdatedAtMs: user.GetUpdatedAtMs(),
|
UpdatedAtMs: user.GetUpdatedAtMs(),
|
||||||
|
ProfileBgImg: user.GetProfileBgImg(),
|
||||||
NextCountryChangeAtMs: nextCountryChangeAtMs,
|
NextCountryChangeAtMs: nextCountryChangeAtMs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -253,7 +253,9 @@ func (h *Handler) confirmGooglePayment(writer http.ResponseWriter, request *http
|
|||||||
if purchaseTimeMS <= 0 {
|
if purchaseTimeMS <= 0 {
|
||||||
purchaseTimeMS = body.PurchaseTimeAlt
|
purchaseTimeMS = body.PurchaseTimeAlt
|
||||||
}
|
}
|
||||||
if commandID == "" || productID <= 0 || packageName == "" || purchaseToken == "" {
|
// 普通充值页继续传 product_id;首充弹窗只持有 Google 商品 ID,
|
||||||
|
// 因此允许 product_id 为空,但必须把 google_product_id 放进 product_code 交给 wallet-service 回查内部商品。
|
||||||
|
if commandID == "" || packageName == "" || purchaseToken == "" || (productID <= 0 && productCode == "") {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
register_device VARCHAR(128) NULL COMMENT '注册设备',
|
register_device VARCHAR(128) NULL COMMENT '注册设备',
|
||||||
os_version VARCHAR(64) NULL COMMENT 'os版本',
|
os_version VARCHAR(64) NULL COMMENT 'os版本',
|
||||||
avatar VARCHAR(512) NULL COMMENT '头像',
|
avatar VARCHAR(512) NULL COMMENT '头像',
|
||||||
|
profile_bg_img VARCHAR(512) NOT NULL DEFAULT '' COMMENT '个人主页信息卡背景图',
|
||||||
birth_date DATE NULL COMMENT '出生日期',
|
birth_date DATE NULL COMMENT '出生日期',
|
||||||
app_version VARCHAR(64) NULL COMMENT '应用版本',
|
app_version VARCHAR(64) NULL COMMENT '应用版本',
|
||||||
build_number VARCHAR(64) NULL COMMENT '构建编号',
|
build_number VARCHAR(64) NULL COMMENT '构建编号',
|
||||||
@ -530,11 +531,14 @@ CREATE TABLE IF NOT EXISTS user_cp_gift_event_consumption (
|
|||||||
application_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '生成或刷新申请 ID',
|
application_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '生成或刷新申请 ID',
|
||||||
relationship_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '命中的 active 关系 ID',
|
relationship_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '命中的 active 关系 ID',
|
||||||
skip_reason VARCHAR(128) NOT NULL DEFAULT '' COMMENT '跳过原因',
|
skip_reason VARCHAR(128) NOT NULL DEFAULT '' COMMENT '跳过原因',
|
||||||
|
gift_value BIGINT NOT NULL DEFAULT 0 COMMENT '本次送礼对 CP 关系贡献的积分',
|
||||||
|
occurred_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'room outbox 事件发生时间,UTC epoch ms',
|
||||||
consumed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '消费时间,UTC epoch ms',
|
consumed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '消费时间,UTC epoch ms',
|
||||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
PRIMARY KEY (app_code, room_event_id),
|
PRIMARY KEY (app_code, room_event_id),
|
||||||
KEY idx_user_cp_gift_consumption_status (app_code, status, updated_at_ms)
|
KEY idx_user_cp_gift_consumption_status (app_code, status, updated_at_ms),
|
||||||
|
KEY idx_user_cp_gift_consumption_weekly_rank (app_code, status, occurred_at_ms, relationship_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 CP 礼物事件消费位点表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 CP 礼物事件消费位点表';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_outbox (
|
CREATE TABLE IF NOT EXISTS user_outbox (
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE user_cp_gift_event_consumption
|
||||||
|
ADD COLUMN gift_value BIGINT NOT NULL DEFAULT 0 COMMENT '本次送礼对 CP 关系贡献的积分' AFTER skip_reason,
|
||||||
|
ADD COLUMN occurred_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'room outbox 事件发生时间,UTC epoch ms' AFTER gift_value;
|
||||||
|
|
||||||
|
CREATE INDEX idx_user_cp_gift_consumption_weekly_rank
|
||||||
|
ON user_cp_gift_event_consumption (app_code, status, occurred_at_ms, relationship_id);
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
SET @add_profile_bg_img := (
|
||||||
|
SELECT IF(
|
||||||
|
COUNT(*) = 0,
|
||||||
|
'ALTER TABLE users ADD COLUMN profile_bg_img VARCHAR(512) NOT NULL DEFAULT '''' COMMENT ''个人主页信息卡背景图'' AFTER avatar',
|
||||||
|
'SELECT 1'
|
||||||
|
)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'users'
|
||||||
|
AND COLUMN_NAME = 'profile_bg_img'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @add_profile_bg_img;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@ -72,6 +72,20 @@ type IntimacyLeaderboardEntry struct {
|
|||||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WeeklyRankEntry 是按一个 UTC 周期聚合的 CP 关系送礼积分;activity-service 用它固化和发奖。
|
||||||
|
type WeeklyRankEntry struct {
|
||||||
|
Rank int64
|
||||||
|
RelationshipID string
|
||||||
|
RelationType string
|
||||||
|
Score int64
|
||||||
|
UserA IntimacyLeaderboardUser
|
||||||
|
UserB IntimacyLeaderboardUser
|
||||||
|
FormedAtMS int64
|
||||||
|
UpdatedAtMS int64
|
||||||
|
FirstScoredAtMS int64
|
||||||
|
LastScoredAtMS int64
|
||||||
|
}
|
||||||
|
|
||||||
// IntimacyLeaderboardPage 是 CP 亲密值排行榜分页读模型。
|
// IntimacyLeaderboardPage 是 CP 亲密值排行榜分页读模型。
|
||||||
type IntimacyLeaderboardPage struct {
|
type IntimacyLeaderboardPage struct {
|
||||||
Items []IntimacyLeaderboardEntry
|
Items []IntimacyLeaderboardEntry
|
||||||
|
|||||||
@ -256,6 +256,8 @@ type User struct {
|
|||||||
RegisterOSVersion string
|
RegisterOSVersion string
|
||||||
// Avatar 是注册入口传入的头像 HTTP(S) URL。
|
// Avatar 是注册入口传入的头像 HTTP(S) URL。
|
||||||
Avatar string
|
Avatar string
|
||||||
|
// ProfileBgImg 是用户主页信息卡背景图片 URL,属于用户主资料,不属于钱包装扮或 profile_card 资源。
|
||||||
|
ProfileBgImg string
|
||||||
// BirthDate 使用 yyyy-mm-dd 字符串表达生日,避免时区把日期偏移成前后一天。
|
// BirthDate 使用 yyyy-mm-dd 字符串表达生日,避免时区把日期偏移成前后一天。
|
||||||
BirthDate string
|
BirthDate string
|
||||||
// RegisterAppVersion 是注册客户端版本。
|
// RegisterAppVersion 是注册客户端版本。
|
||||||
@ -628,6 +630,18 @@ type ProfileUpdateCommand struct {
|
|||||||
UpdatedAtMs int64
|
UpdatedAtMs int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfileBackgroundUpdateCommand 描述用户个人信息页背景图的专用修改。
|
||||||
|
type ProfileBackgroundUpdateCommand struct {
|
||||||
|
// AppCode 是资料修改所属 App。
|
||||||
|
AppCode string
|
||||||
|
// UserID 是要修改背景图的用户,只能来自当前登录态。
|
||||||
|
UserID int64
|
||||||
|
// ProfileBgImg 是对象存储上传后返回的 HTTP(S) 背景图 URL。
|
||||||
|
ProfileBgImg string
|
||||||
|
// UpdatedAtMs 是本次修改时间。
|
||||||
|
UpdatedAtMs int64
|
||||||
|
}
|
||||||
|
|
||||||
// CompleteOnboardingCommand 描述注册页一次性补齐资料的事务输入。
|
// CompleteOnboardingCommand 描述注册页一次性补齐资料的事务输入。
|
||||||
type CompleteOnboardingCommand struct {
|
type CompleteOnboardingCommand struct {
|
||||||
// AppCode 是注册资料固化所属 App。
|
// AppCode 是注册资料固化所属 App。
|
||||||
|
|||||||
@ -29,6 +29,7 @@ type Repository interface {
|
|||||||
CancelBreakRelationship(ctx context.Context, userID int64, relationshipID string, commandID string, reason string, nowMs int64) (cpdomain.RelationshipBreakup, error)
|
CancelBreakRelationship(ctx context.Context, userID int64, relationshipID string, commandID string, reason string, nowMs int64) (cpdomain.RelationshipBreakup, error)
|
||||||
ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent, nowMs int64) (cpdomain.ConsumeResult, error)
|
ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent, nowMs int64) (cpdomain.ConsumeResult, error)
|
||||||
ListActiveIntimacyLeaderboardEntries(ctx context.Context, limit int) ([]cpdomain.IntimacyLeaderboardEntry, error)
|
ListActiveIntimacyLeaderboardEntries(ctx context.Context, limit int) ([]cpdomain.IntimacyLeaderboardEntry, error)
|
||||||
|
ListWeeklyRankEntries(ctx context.Context, relationType string, startMS int64, endMS int64, limit int) ([]cpdomain.WeeklyRankEntry, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IntimacyLeaderboardStore 是 CP 亲密榜 Redis zset 读模型边界。
|
// IntimacyLeaderboardStore 是 CP 亲密榜 Redis zset 读模型边界。
|
||||||
@ -154,16 +155,31 @@ func (s *Service) CancelBreakRelationship(ctx context.Context, userID int64, rel
|
|||||||
// ConsumeGiftEvent 消费 room-service 已扣费并落房间状态的送礼事实。
|
// ConsumeGiftEvent 消费 room-service 已扣费并落房间状态的送礼事实。
|
||||||
func (s *Service) ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent) (cpdomain.ConsumeResult, error) {
|
func (s *Service) ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent) (cpdomain.ConsumeResult, error) {
|
||||||
event.CPRelationType = normalizeRelationType(event.CPRelationType)
|
event.CPRelationType = normalizeRelationType(event.CPRelationType)
|
||||||
if event.CPRelationType == "" {
|
// gift_value 是亲密值和周榜分数增量,沿用 room-service 已扣费后的结算值;0 值仍可创建申请,但不会增加已有关联亲密值。
|
||||||
return cpdomain.ConsumeResult{Consumed: false}, nil
|
|
||||||
}
|
|
||||||
// gift_value 是亲密值增量,沿用 room 热度结算后的值;0 值仍可创建申请,但不会增加已有关联亲密值。
|
|
||||||
if event.GiftValue < 0 {
|
if event.GiftValue < 0 {
|
||||||
event.GiftValue = 0
|
event.GiftValue = 0
|
||||||
}
|
}
|
||||||
return s.repo.ConsumeGiftEvent(ctx, event, s.nowMs())
|
return s.repo.ConsumeGiftEvent(ctx, event, s.nowMs())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListWeeklyRankEntries 给 activity-service 的结算链路读取 CP 周榜快照;它不写 Redis,也不发奖。
|
||||||
|
func (s *Service) ListWeeklyRankEntries(ctx context.Context, relationType string, startMS int64, endMS int64, limit int32) ([]cpdomain.WeeklyRankEntry, int64, error) {
|
||||||
|
if s == nil || s.repo == nil {
|
||||||
|
return nil, 0, xerr.New(xerr.Unavailable, "cp repository is not configured")
|
||||||
|
}
|
||||||
|
if startMS <= 0 || endMS <= startMS {
|
||||||
|
return nil, 0, xerr.New(xerr.InvalidArgument, "weekly rank time window is invalid")
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 3
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
entries, err := s.repo.ListWeeklyRankEntries(ctx, normalizeRelationType(relationType), startMS, endMS, int(limit))
|
||||||
|
return entries, s.nowMs(), err
|
||||||
|
}
|
||||||
|
|
||||||
// RefreshIntimacyLeaderboard 重建 CP 亲密值排行榜读模型;cron 只触发,关系、用户资料和头像框快照都由 owner 服务读取。
|
// RefreshIntimacyLeaderboard 重建 CP 亲密值排行榜读模型;cron 只触发,关系、用户资料和头像框快照都由 owner 服务读取。
|
||||||
func (s *Service) RefreshIntimacyLeaderboard(ctx context.Context, limit int) (int, error) {
|
func (s *Service) RefreshIntimacyLeaderboard(ctx context.Context, limit int) (int, error) {
|
||||||
// 榜单刷新是定时异步链路:任何核心依赖缺失都必须显式失败,让 cron run 记录失败状态,
|
// 榜单刷新是定时异步链路:任何核心依赖缺失都必须显式失败,让 cron run 记录失败状态,
|
||||||
|
|||||||
@ -83,6 +83,10 @@ func (r *fakeLeaderboardRepo) ListActiveIntimacyLeaderboardEntries(context.Conte
|
|||||||
return r.entries, nil
|
return r.entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *fakeLeaderboardRepo) ListWeeklyRankEntries(context.Context, string, int64, int64, int) ([]cpdomain.WeeklyRankEntry, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
type fakeLeaderboardStore struct {
|
type fakeLeaderboardStore struct {
|
||||||
entries []cpdomain.IntimacyLeaderboardEntry
|
entries []cpdomain.IntimacyLeaderboardEntry
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,6 +107,7 @@ type fakeModerationRepository struct {
|
|||||||
sessionIDs []string
|
sessionIDs []string
|
||||||
countrySessionIDs []string
|
countrySessionIDs []string
|
||||||
lastCommand UserStatusCommand
|
lastCommand UserStatusCommand
|
||||||
|
lastProfileCommand userdomain.ProfileUpdateCommand
|
||||||
lastCountryCommand userdomain.CountryChangeCommand
|
lastCountryCommand userdomain.CountryChangeCommand
|
||||||
countryChangeCalls int
|
countryChangeCalls int
|
||||||
lastReport userdomain.Report
|
lastReport userdomain.Report
|
||||||
@ -218,7 +219,13 @@ func (r *fakeModerationRepository) CreateUserWithIdentity(context.Context, userd
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *fakeModerationRepository) UpdateUserProfile(context.Context, userdomain.ProfileUpdateCommand) (userdomain.User, error) {
|
func (r *fakeModerationRepository) UpdateUserProfile(_ context.Context, command userdomain.ProfileUpdateCommand) (userdomain.User, error) {
|
||||||
|
r.lastProfileCommand = command
|
||||||
|
return r.user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fakeModerationRepository) UpdateUserProfileBackground(_ context.Context, command userdomain.ProfileBackgroundUpdateCommand) (userdomain.User, error) {
|
||||||
|
r.user.ProfileBgImg = command.ProfileBgImg
|
||||||
return r.user, nil
|
return r.user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -76,6 +76,42 @@ func (s *Service) UpdateUserProfile(ctx context.Context, userID int64, username
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateUserProfileBackground 只修改个人信息页背景图;客户端传入已上传完成的图片 URL。
|
||||||
|
func (s *Service) UpdateUserProfileBackground(ctx context.Context, userID int64, profileBgImg string) (userdomain.User, error) {
|
||||||
|
if userID <= 0 {
|
||||||
|
// 背景图只能改当前登录用户,gateway 传入 token 中的 user_id,service 层继续做兜底校验。
|
||||||
|
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||||||
|
}
|
||||||
|
if s.userRepository == nil {
|
||||||
|
return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
|
||||||
|
}
|
||||||
|
if err := s.requireCompletedProfile(ctx, userID); err != nil {
|
||||||
|
// 个人信息页背景图属于已完成资料后的用户资料维护,不能绕过注册资料完成流程。
|
||||||
|
return userdomain.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
profileBgImg = strings.TrimSpace(profileBgImg)
|
||||||
|
if profileBgImg == "" {
|
||||||
|
// 上传接口必须写入真实对象 URL;清空背景需要另行设计明确的删除入口。
|
||||||
|
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "profile_bg_img is required")
|
||||||
|
}
|
||||||
|
if userdomain.ProfileStringTooLong(profileBgImg, userdomain.ProfileAvatarMaxRunes) {
|
||||||
|
// users.profile_bg_img 与头像同为 512 字符 URL 字段,超过数据库边界必须在业务层拦截。
|
||||||
|
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "profile_bg_img is too long")
|
||||||
|
}
|
||||||
|
if !userdomain.ValidProfileAvatarURL(profileBgImg) {
|
||||||
|
// 背景图是客户端直接渲染的 URL,不接受资源 code、相对路径或非 HTTP(S) scheme。
|
||||||
|
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "profile_bg_img must be an absolute http or https URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.userRepository.UpdateUserProfileBackground(ctx, userdomain.ProfileBackgroundUpdateCommand{
|
||||||
|
AppCode: appcode.FromContext(ctx),
|
||||||
|
UserID: userID,
|
||||||
|
ProfileBgImg: profileBgImg,
|
||||||
|
UpdatedAtMs: s.now().UnixMilli(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// CompleteOnboarding 是注册页唯一提交入口。
|
// CompleteOnboarding 是注册页唯一提交入口。
|
||||||
// 它必须一次性校验并写入 username/avatar/gender/country/region_id/profile_completed,避免客户端拆接口拼出半完成状态。
|
// 它必须一次性校验并写入 username/avatar/gender/country/region_id/profile_completed,避免客户端拆接口拼出半完成状态。
|
||||||
func (s *Service) CompleteOnboarding(ctx context.Context, userID int64, username string, avatar string, gender string, country string, inviteCode string, requestID string) (userdomain.User, error) {
|
func (s *Service) CompleteOnboarding(ctx context.Context, userID int64, username string, avatar string, gender string, country string, inviteCode string, requestID string) (userdomain.User, error) {
|
||||||
|
|||||||
@ -127,3 +127,39 @@ func TestChangeUserCountryRejectsProtectedRoles(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateUserProfileBackgroundPersistsProfileBgImg(t *testing.T) {
|
||||||
|
repository := &fakeModerationRepository{user: userdomain.User{
|
||||||
|
AppCode: "lalu",
|
||||||
|
UserID: 10001,
|
||||||
|
ProfileCompleted: true,
|
||||||
|
}}
|
||||||
|
svc := New(repository, WithClock(func() time.Time {
|
||||||
|
return time.UnixMilli(1_700_000_000_000).UTC()
|
||||||
|
}))
|
||||||
|
profileBgImg := " https://cdn.example.com/profile-bg.png "
|
||||||
|
|
||||||
|
updated, err := svc.UpdateUserProfileBackground(appcode.WithContext(context.Background(), "lalu"), 10001, profileBgImg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpdateUserProfileBackground failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if updated.ProfileBgImg != "https://cdn.example.com/profile-bg.png" {
|
||||||
|
t.Fatalf("updated profile bg mismatch: %+v", updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateUserProfileBackgroundRejectsInvalidProfileBgImg(t *testing.T) {
|
||||||
|
repository := &fakeModerationRepository{user: userdomain.User{
|
||||||
|
AppCode: "lalu",
|
||||||
|
UserID: 10001,
|
||||||
|
ProfileCompleted: true,
|
||||||
|
}}
|
||||||
|
svc := New(repository)
|
||||||
|
profileBgImg := "profile_card_gold"
|
||||||
|
|
||||||
|
_, err := svc.UpdateUserProfileBackground(appcode.WithContext(context.Background(), "lalu"), 10001, profileBgImg)
|
||||||
|
if xerr.CodeOf(err) != xerr.InvalidArgument {
|
||||||
|
t.Fatalf("invalid profile_bg_img must be rejected, err=%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
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