Compare commits
2 Commits
d92509b5f2
...
15312f498b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15312f498b | ||
|
|
377443eec3 |
@ -19,6 +19,7 @@
|
||||
- 腾讯云 IM 负责客户端长连接、房间群消息、公屏、单聊、离线/漫游和全球接入;
|
||||
- `wallet-service` 负责账务语义。`SendGift` 必须先扣费成功,再落房间表现。
|
||||
- `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;钱包、活动和房间不能直接耦合私有通知投递实现。
|
||||
- `user-service` 负责用户主数据,不应该成为房间高频流程的同步中心。
|
||||
- `cron-service` 只负责后台调度、任务运行记录和通过内部 gRPC 触发 owner service 批处理;不能直接拥有房间、账务、用户或活动业务状态。
|
||||
@ -70,6 +71,7 @@
|
||||
- 修改 `api/proto` 后必须运行 `make proto`,并提交生成后的 `.pb.go` / `_grpc.pb.go`。
|
||||
- 不要在服务之间直接 import 对方 `internal` 包。跨服务只走 protobuf client 或明确的接口抽象。
|
||||
- `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
|
||||
|
||||
|
||||
@ -27,6 +27,13 @@ HY Voice Room 是语音房后端 monorepo。当前架构不再自研 IM 长连
|
||||
- 下游消费 owner outbox 必须使用本服务独立 RocketMQ consumer group,并在本服务库内用幂等表或唯一键记录已消费 `event_id`;消费位点不能写回或复用 owner service 的 outbox 状态。
|
||||
- 后台统计查询面:`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
|
||||
|
||||
项目全局时间模型固定为 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;
|
||||
}
|
||||
|
||||
// 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。
|
||||
service ActivityService {
|
||||
rpc PingActivity(PingActivityRequest) returns (PingActivityResponse);
|
||||
@ -2066,6 +2314,8 @@ service ActivityCronService {
|
||||
rpc ProcessAchievementRewardBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
rpc ProcessRoomTurnoverRewardSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
rpc ProcessWeeklyStarSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
rpc ProcessCPWeeklyRankSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
rpc ProcessAgencyOpeningSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
}
|
||||
|
||||
// TaskService 拥有 App 任务查询、事件进度消费和领奖状态。
|
||||
@ -2112,6 +2362,18 @@ service WeeklyStarService {
|
||||
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.
|
||||
service BroadcastService {
|
||||
rpc EnsureBroadcastGroups(EnsureBroadcastGroupsRequest) returns (EnsureBroadcastGroupsResponse);
|
||||
@ -2206,6 +2468,23 @@ service AdminWeeklyStarService {
|
||||
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 七日签到查询和签到命令。
|
||||
service SevenDayCheckInService {
|
||||
rpc GetSevenDayCheckInStatus(GetSevenDayCheckInStatusRequest) returns (GetSevenDayCheckInStatusResponse);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// - protoc v7.35.0
|
||||
// source: proto/activity/v1/activity.proto
|
||||
|
||||
package activityv1
|
||||
@ -502,6 +502,8 @@ const (
|
||||
ActivityCronService_ProcessAchievementRewardBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessAchievementRewardBatch"
|
||||
ActivityCronService_ProcessRoomTurnoverRewardSettlementBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessRoomTurnoverRewardSettlementBatch"
|
||||
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.
|
||||
@ -515,6 +517,8 @@ type ActivityCronServiceClient interface {
|
||||
ProcessAchievementRewardBatch(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)
|
||||
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 {
|
||||
@ -575,6 +579,26 @@ func (c *activityCronServiceClient) ProcessWeeklyStarSettlementBatch(ctx context
|
||||
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.
|
||||
// All implementations must embed UnimplementedActivityCronServiceServer
|
||||
// for forward compatibility.
|
||||
@ -586,6 +610,8 @@ type ActivityCronServiceServer interface {
|
||||
ProcessAchievementRewardBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
||||
ProcessRoomTurnoverRewardSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
||||
ProcessWeeklyStarSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
||||
ProcessCPWeeklyRankSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
||||
ProcessAgencyOpeningSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error)
|
||||
mustEmbedUnimplementedActivityCronServiceServer()
|
||||
}
|
||||
|
||||
@ -611,6 +637,12 @@ func (UnimplementedActivityCronServiceServer) ProcessRoomTurnoverRewardSettlemen
|
||||
func (UnimplementedActivityCronServiceServer) ProcessWeeklyStarSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
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) testEmbeddedByValue() {}
|
||||
|
||||
@ -722,6 +754,42 @@ func _ActivityCronService_ProcessWeeklyStarSettlementBatch_Handler(srv interface
|
||||
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.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -749,6 +817,14 @@ var ActivityCronService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ProcessWeeklyStarSettlementBatch",
|
||||
Handler: _ActivityCronService_ProcessWeeklyStarSettlementBatch_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ProcessCPWeeklyRankSettlementBatch",
|
||||
Handler: _ActivityCronService_ProcessCPWeeklyRankSettlementBatch_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ProcessAgencyOpeningSettlementBatch",
|
||||
Handler: _ActivityCronService_ProcessAgencyOpeningSettlementBatch_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/activity/v1/activity.proto",
|
||||
@ -1923,6 +1999,294 @@ var WeeklyStarService_ServiceDesc = grpc.ServiceDesc{
|
||||
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 (
|
||||
BroadcastService_EnsureBroadcastGroups_FullMethodName = "/hyapp.activity.v1.BroadcastService/EnsureBroadcastGroups"
|
||||
BroadcastService_PublishRegionBroadcast_FullMethodName = "/hyapp.activity.v1.BroadcastService/PublishRegionBroadcast"
|
||||
@ -4414,6 +4778,486 @@ var AdminWeeklyStarService_ServiceDesc = grpc.ServiceDesc{
|
||||
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 (
|
||||
SevenDayCheckInService_GetSevenDayCheckInStatus_FullMethodName = "/hyapp.activity.v1.SevenDayCheckInService/GetSevenDayCheckInStatus"
|
||||
SevenDayCheckInService_SignSevenDayCheckIn_FullMethodName = "/hyapp.activity.v1.SevenDayCheckInService/SignSevenDayCheckIn"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.29.2
|
||||
// protoc v7.35.0
|
||||
// source: proto/events/room/v1/events.proto
|
||||
|
||||
package roomeventsv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.29.2
|
||||
// protoc v7.35.0
|
||||
// source: proto/game/v1/game.proto
|
||||
|
||||
package gamev1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// - protoc v7.35.0
|
||||
// source: proto/game/v1/game.proto
|
||||
|
||||
package gamev1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.29.2
|
||||
// protoc v7.35.0
|
||||
// source: proto/robot/v1/robot.proto
|
||||
|
||||
package robotv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// - protoc v7.35.0
|
||||
// source: proto/robot/v1/robot.proto
|
||||
|
||||
package robotv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.29.2
|
||||
// protoc v7.35.0
|
||||
// source: proto/room/v1/room.proto
|
||||
|
||||
package roomv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// - protoc v7.35.0
|
||||
// source: proto/room/v1/room.proto
|
||||
|
||||
package roomv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.29.2
|
||||
// protoc v7.35.0
|
||||
// source: proto/user/v1/auth.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// - protoc v7.35.0
|
||||
// source: proto/user/v1/auth.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.29.2
|
||||
// protoc v7.35.0
|
||||
// source: proto/user/v1/host.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// - protoc v7.35.0
|
||||
// source: proto/user/v1/host.proto
|
||||
|
||||
package userv1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -85,6 +85,7 @@ message User {
|
||||
InviteOverview invite = 28;
|
||||
string pretty_id = 29;
|
||||
string pretty_display_user_id = 30;
|
||||
string profile_bg_img = 31;
|
||||
}
|
||||
|
||||
// InviteOverview 是我的页可直接展示的邀请码和邀请计数 read model。
|
||||
@ -476,6 +477,33 @@ message CPIntimacyLeaderboardItem {
|
||||
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 {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
@ -693,6 +721,18 @@ message UpdateUserProfileResponse {
|
||||
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 用户自助修改国家入口;受限业务身份不允许自助修改。
|
||||
message ChangeUserCountryRequest {
|
||||
RequestMeta meta = 1;
|
||||
@ -1231,6 +1271,7 @@ service UserService {
|
||||
rpc ListUserIDs(ListUserIDsRequest) returns (ListUserIDsResponse);
|
||||
rpc GetUserMicLifetimeStats(GetUserMicLifetimeStatsRequest) returns (GetUserMicLifetimeStatsResponse);
|
||||
rpc UpdateUserProfile(UpdateUserProfileRequest) returns (UpdateUserProfileResponse);
|
||||
rpc UpdateUserProfileBackground(UpdateUserProfileBackgroundRequest) returns (UpdateUserProfileBackgroundResponse);
|
||||
rpc ChangeUserCountry(ChangeUserCountryRequest) returns (ChangeUserCountryResponse);
|
||||
rpc AdminChangeUserCountry(AdminChangeUserCountryRequest) returns (ChangeUserCountryResponse);
|
||||
rpc SetUserStatus(SetUserStatusRequest) returns (SetUserStatusResponse);
|
||||
@ -1270,6 +1311,7 @@ service UserCPService {
|
||||
// UserCPInternalService 给 owner outbox 消费 worker 调用,不暴露给 App 或后台。
|
||||
service UserCPInternalService {
|
||||
rpc ConsumeRoomGiftCPEvent(ConsumeRoomGiftCPEventRequest) returns (ConsumeRoomGiftCPEventResponse);
|
||||
rpc ListCPWeeklyRankEntries(ListCPWeeklyRankEntriesRequest) returns (ListCPWeeklyRankEntriesResponse);
|
||||
}
|
||||
|
||||
// UserCronService 只给 cron-service 调用,业务状态仍由 user-service owner 修改。
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// - protoc v7.35.0
|
||||
// source: proto/user/v1/user.proto
|
||||
|
||||
package userv1
|
||||
@ -19,21 +19,22 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
UserService_GetUser_FullMethodName = "/hyapp.user.v1.UserService/GetUser"
|
||||
UserService_GetInviteAttribution_FullMethodName = "/hyapp.user.v1.UserService/GetInviteAttribution"
|
||||
UserService_BusinessUserLookup_FullMethodName = "/hyapp.user.v1.UserService/BusinessUserLookup"
|
||||
UserService_GetMyProfileStats_FullMethodName = "/hyapp.user.v1.UserService/GetMyProfileStats"
|
||||
UserService_BatchGetUsers_FullMethodName = "/hyapp.user.v1.UserService/BatchGetUsers"
|
||||
UserService_ListUserIDs_FullMethodName = "/hyapp.user.v1.UserService/ListUserIDs"
|
||||
UserService_GetUserMicLifetimeStats_FullMethodName = "/hyapp.user.v1.UserService/GetUserMicLifetimeStats"
|
||||
UserService_UpdateUserProfile_FullMethodName = "/hyapp.user.v1.UserService/UpdateUserProfile"
|
||||
UserService_ChangeUserCountry_FullMethodName = "/hyapp.user.v1.UserService/ChangeUserCountry"
|
||||
UserService_AdminChangeUserCountry_FullMethodName = "/hyapp.user.v1.UserService/AdminChangeUserCountry"
|
||||
UserService_SetUserStatus_FullMethodName = "/hyapp.user.v1.UserService/SetUserStatus"
|
||||
UserService_CreateManagerUserBlock_FullMethodName = "/hyapp.user.v1.UserService/CreateManagerUserBlock"
|
||||
UserService_ListManagerUserBlocks_FullMethodName = "/hyapp.user.v1.UserService/ListManagerUserBlocks"
|
||||
UserService_UnblockManagerUser_FullMethodName = "/hyapp.user.v1.UserService/UnblockManagerUser"
|
||||
UserService_CompleteOnboarding_FullMethodName = "/hyapp.user.v1.UserService/CompleteOnboarding"
|
||||
UserService_GetUser_FullMethodName = "/hyapp.user.v1.UserService/GetUser"
|
||||
UserService_GetInviteAttribution_FullMethodName = "/hyapp.user.v1.UserService/GetInviteAttribution"
|
||||
UserService_BusinessUserLookup_FullMethodName = "/hyapp.user.v1.UserService/BusinessUserLookup"
|
||||
UserService_GetMyProfileStats_FullMethodName = "/hyapp.user.v1.UserService/GetMyProfileStats"
|
||||
UserService_BatchGetUsers_FullMethodName = "/hyapp.user.v1.UserService/BatchGetUsers"
|
||||
UserService_ListUserIDs_FullMethodName = "/hyapp.user.v1.UserService/ListUserIDs"
|
||||
UserService_GetUserMicLifetimeStats_FullMethodName = "/hyapp.user.v1.UserService/GetUserMicLifetimeStats"
|
||||
UserService_UpdateUserProfile_FullMethodName = "/hyapp.user.v1.UserService/UpdateUserProfile"
|
||||
UserService_UpdateUserProfileBackground_FullMethodName = "/hyapp.user.v1.UserService/UpdateUserProfileBackground"
|
||||
UserService_ChangeUserCountry_FullMethodName = "/hyapp.user.v1.UserService/ChangeUserCountry"
|
||||
UserService_AdminChangeUserCountry_FullMethodName = "/hyapp.user.v1.UserService/AdminChangeUserCountry"
|
||||
UserService_SetUserStatus_FullMethodName = "/hyapp.user.v1.UserService/SetUserStatus"
|
||||
UserService_CreateManagerUserBlock_FullMethodName = "/hyapp.user.v1.UserService/CreateManagerUserBlock"
|
||||
UserService_ListManagerUserBlocks_FullMethodName = "/hyapp.user.v1.UserService/ListManagerUserBlocks"
|
||||
UserService_UnblockManagerUser_FullMethodName = "/hyapp.user.v1.UserService/UnblockManagerUser"
|
||||
UserService_CompleteOnboarding_FullMethodName = "/hyapp.user.v1.UserService/CompleteOnboarding"
|
||||
)
|
||||
|
||||
// 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)
|
||||
GetUserMicLifetimeStats(ctx context.Context, in *GetUserMicLifetimeStatsRequest, opts ...grpc.CallOption) (*GetUserMicLifetimeStatsResponse, 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)
|
||||
AdminChangeUserCountry(ctx context.Context, in *AdminChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, 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
|
||||
}
|
||||
|
||||
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) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ChangeUserCountryResponse)
|
||||
@ -231,6 +243,7 @@ type UserServiceServer interface {
|
||||
ListUserIDs(context.Context, *ListUserIDsRequest) (*ListUserIDsResponse, error)
|
||||
GetUserMicLifetimeStats(context.Context, *GetUserMicLifetimeStatsRequest) (*GetUserMicLifetimeStatsResponse, error)
|
||||
UpdateUserProfile(context.Context, *UpdateUserProfileRequest) (*UpdateUserProfileResponse, error)
|
||||
UpdateUserProfileBackground(context.Context, *UpdateUserProfileBackgroundRequest) (*UpdateUserProfileBackgroundResponse, error)
|
||||
ChangeUserCountry(context.Context, *ChangeUserCountryRequest) (*ChangeUserCountryResponse, error)
|
||||
AdminChangeUserCountry(context.Context, *AdminChangeUserCountryRequest) (*ChangeUserCountryResponse, error)
|
||||
SetUserStatus(context.Context, *SetUserStatusRequest) (*SetUserStatusResponse, error)
|
||||
@ -272,6 +285,9 @@ func (UnimplementedUserServiceServer) GetUserMicLifetimeStats(context.Context, *
|
||||
func (UnimplementedUserServiceServer) UpdateUserProfile(context.Context, *UpdateUserProfileRequest) (*UpdateUserProfileResponse, error) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
in := new(ChangeUserCountryRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -623,6 +657,10 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "UpdateUserProfile",
|
||||
Handler: _UserService_UpdateUserProfile_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpdateUserProfileBackground",
|
||||
Handler: _UserService_UpdateUserProfileBackground_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ChangeUserCountry",
|
||||
Handler: _UserService_ChangeUserCountry_Handler,
|
||||
@ -1515,7 +1553,8 @@ var UserCPService_ServiceDesc = grpc.ServiceDesc{
|
||||
}
|
||||
|
||||
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.
|
||||
@ -1525,6 +1564,7 @@ const (
|
||||
// UserCPInternalService 给 owner outbox 消费 worker 调用,不暴露给 App 或后台。
|
||||
type UserCPInternalServiceClient interface {
|
||||
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 {
|
||||
@ -1545,6 +1585,16 @@ func (c *userCPInternalServiceClient) ConsumeRoomGiftCPEvent(ctx context.Context
|
||||
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.
|
||||
// All implementations must embed UnimplementedUserCPInternalServiceServer
|
||||
// for forward compatibility.
|
||||
@ -1552,6 +1602,7 @@ func (c *userCPInternalServiceClient) ConsumeRoomGiftCPEvent(ctx context.Context
|
||||
// UserCPInternalService 给 owner outbox 消费 worker 调用,不暴露给 App 或后台。
|
||||
type UserCPInternalServiceServer interface {
|
||||
ConsumeRoomGiftCPEvent(context.Context, *ConsumeRoomGiftCPEventRequest) (*ConsumeRoomGiftCPEventResponse, error)
|
||||
ListCPWeeklyRankEntries(context.Context, *ListCPWeeklyRankEntriesRequest) (*ListCPWeeklyRankEntriesResponse, error)
|
||||
mustEmbedUnimplementedUserCPInternalServiceServer()
|
||||
}
|
||||
|
||||
@ -1565,6 +1616,9 @@ type UnimplementedUserCPInternalServiceServer struct{}
|
||||
func (UnimplementedUserCPInternalServiceServer) ConsumeRoomGiftCPEvent(context.Context, *ConsumeRoomGiftCPEventRequest) (*ConsumeRoomGiftCPEventResponse, error) {
|
||||
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) testEmbeddedByValue() {}
|
||||
|
||||
@ -1604,6 +1658,24 @@ func _UserCPInternalService_ConsumeRoomGiftCPEvent_Handler(srv interface{}, ctx
|
||||
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.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -1615,6 +1687,10 @@ var UserCPInternalService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ConsumeRoomGiftCPEvent",
|
||||
Handler: _UserCPInternalService_ConsumeRoomGiftCPEvent_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListCPWeeklyRankEntries",
|
||||
Handler: _UserCPInternalService_ListCPWeeklyRankEntries_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 唯一可以调用的钱包游戏改账入口。
|
||||
message ApplyGameCoinChangeRequest {
|
||||
string request_id = 1;
|
||||
@ -1913,6 +1935,7 @@ service WalletService {
|
||||
rpc CreditLuckyGiftReward(CreditLuckyGiftRewardRequest) returns (CreditLuckyGiftRewardResponse);
|
||||
rpc CreditRoomTurnoverReward(CreditRoomTurnoverRewardRequest) returns (CreditRoomTurnoverRewardResponse);
|
||||
rpc CreditInviteActivityReward(CreditInviteActivityRewardRequest) returns (CreditInviteActivityRewardResponse);
|
||||
rpc CreditAgencyOpeningReward(CreditAgencyOpeningRewardRequest) returns (CreditAgencyOpeningRewardResponse);
|
||||
rpc ApplyGameCoinChange(ApplyGameCoinChangeRequest) returns (ApplyGameCoinChangeResponse);
|
||||
rpc GetRedPacketConfig(GetRedPacketConfigRequest) returns (GetRedPacketConfigResponse);
|
||||
rpc UpdateRedPacketConfig(UpdateRedPacketConfigRequest) returns (UpdateRedPacketConfigResponse);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// - protoc v7.35.0
|
||||
// source: proto/wallet/v1/wallet.proto
|
||||
|
||||
package walletv1
|
||||
@ -272,6 +272,7 @@ const (
|
||||
WalletService_CreditLuckyGiftReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditLuckyGiftReward"
|
||||
WalletService_CreditRoomTurnoverReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditRoomTurnoverReward"
|
||||
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_GetRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRedPacketConfig"
|
||||
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)
|
||||
CreditRoomTurnoverReward(ctx context.Context, in *CreditRoomTurnoverRewardRequest, opts ...grpc.CallOption) (*CreditRoomTurnoverRewardResponse, 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)
|
||||
GetRedPacketConfig(ctx context.Context, in *GetRedPacketConfigRequest, opts ...grpc.CallOption) (*GetRedPacketConfigResponse, 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
|
||||
}
|
||||
|
||||
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) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ApplyGameCoinChangeResponse)
|
||||
@ -1256,6 +1268,7 @@ type WalletServiceServer interface {
|
||||
CreditLuckyGiftReward(context.Context, *CreditLuckyGiftRewardRequest) (*CreditLuckyGiftRewardResponse, error)
|
||||
CreditRoomTurnoverReward(context.Context, *CreditRoomTurnoverRewardRequest) (*CreditRoomTurnoverRewardResponse, error)
|
||||
CreditInviteActivityReward(context.Context, *CreditInviteActivityRewardRequest) (*CreditInviteActivityRewardResponse, error)
|
||||
CreditAgencyOpeningReward(context.Context, *CreditAgencyOpeningRewardRequest) (*CreditAgencyOpeningRewardResponse, error)
|
||||
ApplyGameCoinChange(context.Context, *ApplyGameCoinChangeRequest) (*ApplyGameCoinChangeResponse, error)
|
||||
GetRedPacketConfig(context.Context, *GetRedPacketConfigRequest) (*GetRedPacketConfigResponse, error)
|
||||
UpdateRedPacketConfig(context.Context, *UpdateRedPacketConfigRequest) (*UpdateRedPacketConfigResponse, error)
|
||||
@ -1488,6 +1501,9 @@ func (UnimplementedWalletServiceServer) CreditRoomTurnoverReward(context.Context
|
||||
func (UnimplementedWalletServiceServer) CreditInviteActivityReward(context.Context, *CreditInviteActivityRewardRequest) (*CreditInviteActivityRewardResponse, error) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
in := new(ApplyGameCoinChangeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -3267,6 +3301,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "CreditInviteActivityReward",
|
||||
Handler: _WalletService_CreditInviteActivityReward_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CreditAgencyOpeningReward",
|
||||
Handler: _WalletService_CreditAgencyOpeningReward_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ApplyGameCoinChange",
|
||||
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 都不能佩戴。 |
|
||||
| 发放默认佩戴 | `badge`、`mic_seat_animation` 发放成功后默认佩戴。 |
|
||||
| 徽章展示 | `badge` 是拥有即佩戴的多装备资源;资料页多徽章展示仍使用 `/api/v1/badges/me` 和 `/api/v1/badges/display`。 |
|
||||
| 资料卡背景 | `profile_card` 只用于装扮资料卡;个人主页背景图使用用户资料字段 `profile_bg_img`,不走背包佩戴。 |
|
||||
|
||||
常见错误:
|
||||
|
||||
|
||||
@ -866,7 +866,7 @@ paths:
|
||||
- users
|
||||
summary: 修改当前用户基础资料
|
||||
operationId: updateMyProfile
|
||||
description: 只修改 `username`、`avatar`、`birth`;国家修改必须走独立国家接口。
|
||||
description: 只修改 `username`、`avatar`、`birth`;主页背景图和国家修改都走独立接口。
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
@ -890,6 +890,36 @@ paths:
|
||||
$ref: "#/responses/Internal"
|
||||
"502":
|
||||
$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:
|
||||
post:
|
||||
tags:
|
||||
@ -3144,6 +3174,9 @@ definitions:
|
||||
updated_at_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
profile_bg_img:
|
||||
type: string
|
||||
description: 用户主页信息卡背景图片 URL,来自 users.profile_bg_img;和 `profile_card` 装扮资源无关,未设置时为空字符串。
|
||||
next_country_change_allowed_at_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
@ -3594,6 +3627,28 @@ definitions:
|
||||
birth:
|
||||
type: string
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@ -927,6 +927,9 @@ definitions:
|
||||
description: 用户选择的国家主数据 country_code。
|
||||
avatar:
|
||||
type: string
|
||||
profile_bg_img:
|
||||
type: string
|
||||
description: 用户主页信息卡背景图片 URL,属于用户资料字段;和 `profile_card` 装扮资源无关。
|
||||
birth:
|
||||
type: string
|
||||
description: yyyy-mm-dd。
|
||||
@ -1085,6 +1088,22 @@ definitions:
|
||||
properties:
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@ -27,7 +27,8 @@ Authorization: Bearer <access_token>
|
||||
"avatar": "https://cdn.example.com/avatar.png",
|
||||
"language": "en",
|
||||
"profile_completed": true,
|
||||
"onboarding_status": "completed"
|
||||
"onboarding_status": "completed",
|
||||
"profile_bg_img": "https://cdn.example.com/profile-bg.png"
|
||||
},
|
||||
"profile_stats": {
|
||||
"visitors_count": 12,
|
||||
@ -97,6 +98,7 @@ Authorization: Bearer <access_token>
|
||||
| Field | Source | Rule |
|
||||
| --- | --- | --- |
|
||||
| `profile` | `user-service.GetUser` | 只取头像、昵称、展示 ID、语言等基础资料 |
|
||||
| `profile.profile_bg_img` | `user-service.GetUser` | 取用户资料里的主页背景图;和 `profile_card` 装扮资源无关,未设置返回空字符串 |
|
||||
| `profile_stats` | `user-service.GetMyProfileStats` | 读 `user_profile_stats` 计数表,不实时扫访问、关注、好友大表 |
|
||||
| `wallet` | `wallet-service.GetWalletValueSummary` | 首屏只返回金币余额,不返回钻石和美元余额 |
|
||||
| `vip` | `wallet-service.GetMyVip` | 只取等级、名称、有效期和 active 状态 |
|
||||
|
||||
@ -523,5 +523,6 @@ Flutter 处理:
|
||||
2. 背包页调 `/api/v1/users/me/resources?resource_type=xxx`。
|
||||
3. 点击佩戴调 `POST /api/v1/users/me/resources/{resource_id}/equip`,成功后刷新 `/appearance`。
|
||||
4. 点击取消佩戴调 `DELETE /api/v1/users/me/resources/{resource_type}/unequip`,成功后刷新 `/appearance`。
|
||||
5. 房间、公屏、在线用户资料优先使用 `room-display-profiles:batch` 的 `avatar_frame`、`profile_card`、`vehicle`、`badges`。
|
||||
6. 收到 `room_user_joined` IM 时,只按 `entry_vehicle` 播放入场座驾。
|
||||
5. 个人主页信息卡背景使用个人资料返回的 `profile_bg_img`;该字段是用户资料字段,和 `profile_card` 装扮资源无关。
|
||||
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
|
||||
|
||||
访问资料卡:
|
||||
|
||||
```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
|
||||
|
||||
@ -87,6 +87,8 @@ go run ./cmd/server -config configs/config.yaml -bootstrap
|
||||
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 时顺手修改用户区域。
|
||||
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` 保存,并在后台用户列表/导出查询中执行。
|
||||
- 任务产物默认写入 `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
|
||||
|
||||
```bash
|
||||
|
||||
@ -26,6 +26,7 @@ import (
|
||||
"hyapp-admin-server/internal/migration"
|
||||
achievementconfigmodule "hyapp-admin-server/internal/modules/achievementconfig"
|
||||
adminusermodule "hyapp-admin-server/internal/modules/adminuser"
|
||||
agencyopeningmodule "hyapp-admin-server/internal/modules/agencyopening"
|
||||
appconfigmodule "hyapp-admin-server/internal/modules/appconfig"
|
||||
appregistrymodule "hyapp-admin-server/internal/modules/appregistry"
|
||||
appusermodule "hyapp-admin-server/internal/modules/appuser"
|
||||
@ -38,6 +39,7 @@ import (
|
||||
dailytaskmodule "hyapp-admin-server/internal/modules/dailytask"
|
||||
dashboardmodule "hyapp-admin-server/internal/modules/dashboard"
|
||||
firstrechargerewardmodule "hyapp-admin-server/internal/modules/firstrechargereward"
|
||||
fullservernoticemodule "hyapp-admin-server/internal/modules/fullservernotice"
|
||||
gamemanagementmodule "hyapp-admin-server/internal/modules/gamemanagement"
|
||||
giftdiamondmodule "hyapp-admin-server/internal/modules/giftdiamond"
|
||||
healthmodule "hyapp-admin-server/internal/modules/health"
|
||||
@ -252,17 +254,19 @@ func main() {
|
||||
Audit: auditHandler,
|
||||
Auth: authmodule.New(store, auth, auditHandler, cfg),
|
||||
AdminUser: adminusermodule.New(store, cfg, auditHandler),
|
||||
AgencyOpening: agencyopeningmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||
AchievementConfig: achievementconfigmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||
AppConfig: appconfigmodule.New(store, auditHandler),
|
||||
AppRegistry: appregistrymodule.New(userDB),
|
||||
AppUser: appusermodule.New(userclient.NewGRPC(userConn), activityclient.NewGRPC(activityConn), sqlDB, userDB, walletDB, cfg, auditHandler),
|
||||
CoinLedger: coinledgermodule.New(userDB, walletDB, sqlDB, walletclient.NewGRPC(walletConn), 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),
|
||||
DailyTask: dailytaskmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||
Dashboard: dashboardmodule.New(store, cfg, userclient.NewGRPC(userConn)),
|
||||
FirstRechargeReward: firstrechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||
FullServerNotice: fullservernoticemodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||
Game: gamemanagementmodule.New(
|
||||
gameclient.NewGRPC(gameConn),
|
||||
userclient.NewGRPC(userConn),
|
||||
|
||||
@ -42,6 +42,15 @@ type Client interface {
|
||||
SetWeeklyStarCycleStatus(ctx context.Context, req *activityv1.SetWeeklyStarCycleStatusRequest) (*activityv1.SetWeeklyStarCycleStatusResponse, error)
|
||||
ListWeeklyStarLeaderboard(ctx context.Context, req *activityv1.ListWeeklyStarLeaderboardRequest) (*activityv1.ListWeeklyStarLeaderboardResponse, 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)
|
||||
UpsertLuckyGiftConfig(ctx context.Context, req *activityv1.UpsertLuckyGiftConfigRequest) (*activityv1.UpsertLuckyGiftConfigResponse, 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)
|
||||
UpsertLevelRule(ctx context.Context, req *activityv1.UpsertLevelRuleRequest) (*activityv1.UpsertLevelRuleResponse, 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 {
|
||||
@ -64,9 +75,12 @@ type GRPCClient struct {
|
||||
sevenDayCheckInClient activityv1.AdminSevenDayCheckInServiceClient
|
||||
roomTurnoverRewardClient activityv1.AdminRoomTurnoverRewardServiceClient
|
||||
weeklyStarClient activityv1.AdminWeeklyStarServiceClient
|
||||
agencyOpeningClient activityv1.AdminAgencyOpeningServiceClient
|
||||
cpWeeklyRankClient activityv1.AdminCPWeeklyRankServiceClient
|
||||
luckyGiftClient activityv1.AdminLuckyGiftServiceClient
|
||||
broadcastClient activityv1.BroadcastServiceClient
|
||||
growthClient activityv1.AdminGrowthLevelServiceClient
|
||||
messageClient activityv1.MessageInboxServiceClient
|
||||
}
|
||||
|
||||
func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
|
||||
@ -80,9 +94,12 @@ func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
|
||||
sevenDayCheckInClient: activityv1.NewAdminSevenDayCheckInServiceClient(conn),
|
||||
roomTurnoverRewardClient: activityv1.NewAdminRoomTurnoverRewardServiceClient(conn),
|
||||
weeklyStarClient: activityv1.NewAdminWeeklyStarServiceClient(conn),
|
||||
agencyOpeningClient: activityv1.NewAdminAgencyOpeningServiceClient(conn),
|
||||
cpWeeklyRankClient: activityv1.NewAdminCPWeeklyRankServiceClient(conn),
|
||||
luckyGiftClient: activityv1.NewAdminLuckyGiftServiceClient(conn),
|
||||
broadcastClient: activityv1.NewBroadcastServiceClient(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)
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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"
|
||||
|
||||
"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"
|
||||
|
||||
@ -17,7 +19,11 @@ type Handler struct {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -35,6 +41,9 @@ func (h *Handler) UpdateConfig(c *gin.Context) {
|
||||
response.BadRequest(c, "CP配置参数不正确")
|
||||
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)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrInvalidArgument) {
|
||||
|
||||
@ -8,6 +8,9 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapp-admin-server/internal/integration/activityclient"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -38,12 +41,14 @@ type DB interface {
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
db DB
|
||||
db DB
|
||||
activity activityclient.Client
|
||||
}
|
||||
|
||||
type configDTO struct {
|
||||
AppCode string `json:"appCode"`
|
||||
Relations []relationDTO `json:"relations"`
|
||||
WeeklyRank weeklyRankDTO `json:"weeklyRank"`
|
||||
ServerTimeMS int64 `json:"serverTimeMs"`
|
||||
}
|
||||
|
||||
@ -67,12 +72,28 @@ type levelDTO struct {
|
||||
UpdatedAtMS int64 `json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
type updateRequest struct {
|
||||
Relations []relationDTO `json:"relations"`
|
||||
type weeklyRankDTO struct {
|
||||
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 {
|
||||
return &Service{db: db}
|
||||
type weeklyRankRewardDTO struct {
|
||||
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 的关系容量和等级阈值;缺配置时先落默认值,保证后台首次打开就是可保存状态。
|
||||
@ -88,7 +109,11 @@ func (s *Service) GetConfig(ctx context.Context, appCode string) (configDTO, err
|
||||
if err != nil {
|
||||
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,运行中消费逻辑每次读取最新阈值。
|
||||
@ -146,9 +171,80 @@ func (s *Service) UpdateConfig(ctx context.Context, appCode string, req updateRe
|
||||
if err := tx.Commit(); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
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: "gift-diamond:view", Kind: "menu"},
|
||||
{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-third-party:view", Kind: "menu"},
|
||||
{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:update", 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:create", 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: "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-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-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},
|
||||
@ -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: "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: "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: "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},
|
||||
@ -530,7 +537,7 @@ func defaultRolePermissionCodes(code string) []string {
|
||||
"agency:view", "agency:create", "agency:status", "agency:delete",
|
||||
"bd:view", "bd:create", "bd:update",
|
||||
"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",
|
||||
"game:view", "game:create", "game:update", "game:status", "game:delete",
|
||||
"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",
|
||||
"vip-config:view", "vip-config:update", "vip-config:grant",
|
||||
"weekly-star:view", "weekly-star:create", "weekly-star:update", "weekly-star:settle",
|
||||
"agency-opening:view", "agency-opening:create", "agency-opening:update",
|
||||
"log:view",
|
||||
"job:view", "job:cancel", "export:create",
|
||||
"upload:create",
|
||||
}
|
||||
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":
|
||||
return []string{
|
||||
"overview:view",
|
||||
@ -581,6 +589,7 @@ func defaultRolePermissionCodes(code string) []string {
|
||||
"coin-adjustment:view",
|
||||
"report:view",
|
||||
"gift-diamond:view",
|
||||
"full-server-notice:view",
|
||||
"lucky-gift:view",
|
||||
"payment-bill:view",
|
||||
"payment-third-party:view",
|
||||
@ -594,6 +603,7 @@ func defaultRolePermissionCodes(code string) []string {
|
||||
"cp-config:view",
|
||||
"vip-config:view",
|
||||
"weekly-star:view",
|
||||
"agency-opening:view",
|
||||
"role:view",
|
||||
"permission:view",
|
||||
"menu:view",
|
||||
@ -627,7 +637,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
||||
"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",
|
||||
"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",
|
||||
"game:view", "game:create", "game:update", "game:status", "game:delete",
|
||||
"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",
|
||||
"vip-config:view", "vip-config:update", "vip-config:grant",
|
||||
"weekly-star:view", "weekly-star:create", "weekly-star:update", "weekly-star:settle",
|
||||
"agency-opening:view", "agency-opening:create", "agency-opening:update",
|
||||
}
|
||||
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:
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
"hyapp-admin-server/internal/modules/achievementconfig"
|
||||
"hyapp-admin-server/internal/modules/adminuser"
|
||||
"hyapp-admin-server/internal/modules/agencyopening"
|
||||
"hyapp-admin-server/internal/modules/appconfig"
|
||||
"hyapp-admin-server/internal/modules/appregistry"
|
||||
"hyapp-admin-server/internal/modules/appuser"
|
||||
@ -17,6 +18,7 @@ import (
|
||||
"hyapp-admin-server/internal/modules/dailytask"
|
||||
"hyapp-admin-server/internal/modules/dashboard"
|
||||
"hyapp-admin-server/internal/modules/firstrechargereward"
|
||||
"hyapp-admin-server/internal/modules/fullservernotice"
|
||||
gamemanagement "hyapp-admin-server/internal/modules/gamemanagement"
|
||||
"hyapp-admin-server/internal/modules/giftdiamond"
|
||||
"hyapp-admin-server/internal/modules/health"
|
||||
@ -55,6 +57,7 @@ import (
|
||||
|
||||
type Handlers struct {
|
||||
AdminUser *adminuser.Handler
|
||||
AgencyOpening *agencyopening.Handler
|
||||
AchievementConfig *achievementconfig.Handler
|
||||
AppConfig *appconfig.Handler
|
||||
AppRegistry *appregistry.Handler
|
||||
@ -68,6 +71,7 @@ type Handlers struct {
|
||||
DailyTask *dailytask.Handler
|
||||
Dashboard *dashboard.Handler
|
||||
FirstRechargeReward *firstrechargereward.Handler
|
||||
FullServerNotice *fullservernotice.Handler
|
||||
Game *gamemanagement.Handler
|
||||
GiftDiamond *giftdiamond.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))
|
||||
|
||||
authroutes.RegisterRoutes(api, protected, h.Auth)
|
||||
agencyopening.RegisterRoutes(protected, h.AgencyOpening)
|
||||
achievementconfig.RegisterRoutes(protected, h.AchievementConfig)
|
||||
coinledger.RegisterRoutes(protected, h.CoinLedger)
|
||||
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)
|
||||
dailytask.RegisterRoutes(protected, h.DailyTask)
|
||||
firstrechargereward.RegisterRoutes(protected, h.FirstRechargeReward)
|
||||
fullservernotice.RegisterRoutes(protected, h.FullServerNotice)
|
||||
registrationreward.RegisterRoutes(protected, h.RegistrationReward)
|
||||
regionblock.RegisterRoutes(protected, h.RegionBlock)
|
||||
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)
|
||||
) 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 (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' 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))
|
||||
// cron 入口只做调度适配;具体状态机仍在各 activity service 内部。
|
||||
// weekly-star 和 room-turnover 都依赖 wallet 发奖,所以这里必须把同一批已装配的 service 注入 cron server。
|
||||
activityv1.RegisterActivityCronServiceServer(server, grpcserver.NewCronServer(services.message, services.growth, services.achievement, services.weeklyStar, services.roomTurnoverReward))
|
||||
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.RegisterAdminTaskServiceServer(server, grpcserver.NewAdminTaskServer(services.task))
|
||||
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.RegisterWeeklyStarServiceServer(server, grpcserver.NewWeeklyStarServer(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.RegisterAdminRoomTurnoverRewardServiceServer(server, grpcserver.NewAdminRoomTurnoverRewardServer(services.roomTurnoverReward))
|
||||
// BroadcastServer 同时承载 BroadcastService 和 RoomEventConsumerService。
|
||||
// gRPC 直连投递和 MQ 消费都必须经过相同的 room event fanout 顺序,否则本地验证会通过但线上 MQ 路径漏积分。
|
||||
broadcastServer := grpcserver.NewBroadcastServer(services.broadcast, services.growth, services.weeklyStar, services.task, services.roomTurnoverReward)
|
||||
broadcastServer := grpcserver.NewBroadcastServer(services.broadcast, services.growth, services.weeklyStar, services.task, services.roomTurnoverReward, services.agencyOpening)
|
||||
activityv1.RegisterBroadcastServiceServer(server, broadcastServer)
|
||||
activityv1.RegisterRoomEventConsumerServiceServer(server, broadcastServer)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if _, err := services.agencyOpening.HandleRoomEvent(eventCtx, envelope); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := services.task.HandleRoomEvent(eventCtx, envelope); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -10,7 +10,9 @@ import (
|
||||
"hyapp/services/activity-service/internal/config"
|
||||
achievementservice "hyapp/services/activity-service/internal/service/achievement"
|
||||
activityservice "hyapp/services/activity-service/internal/service/activity"
|
||||
agencyopeningservice "hyapp/services/activity-service/internal/service/agencyopening"
|
||||
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
||||
cpweeklyrankservice "hyapp/services/activity-service/internal/service/cpweeklyrank"
|
||||
cumulativerechargeservice "hyapp/services/activity-service/internal/service/cumulativerecharge"
|
||||
firstrechargeservice "hyapp/services/activity-service/internal/service/firstrecharge"
|
||||
growthservice "hyapp/services/activity-service/internal/service/growth"
|
||||
@ -33,7 +35,9 @@ type serviceBundle struct {
|
||||
sevenDayCheckIn *sevendaycheckinservice.Service
|
||||
growth *growthservice.Service
|
||||
achievement *achievementservice.Service
|
||||
agencyOpening *agencyopeningservice.Service
|
||||
weeklyStar *weeklystarservice.Service
|
||||
cpWeeklyRank *cpweeklyrankservice.Service
|
||||
roomTurnoverReward *roomturnoverrewardservice.Service
|
||||
broadcast *broadcastservice.Service
|
||||
luckyGift *luckygiftservice.Service
|
||||
@ -54,7 +58,9 @@ func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository,
|
||||
sevenDayCheckInSvc := sevendaycheckinservice.New(repository, walletClient)
|
||||
growthSvc := growthservice.New(repository, walletClient)
|
||||
achievementSvc := achievementservice.New(repository, walletClient)
|
||||
agencyOpeningSvc := agencyopeningservice.New(repository, walletClient, activityclient.NewGRPCAgencyOpeningSource(clients.userConn), roomClient)
|
||||
weeklyStarSvc := weeklystarservice.New(repository, walletClient)
|
||||
cpWeeklyRankSvc := cpweeklyrankservice.New(repository, activityclient.NewGRPCCPWeeklyRankSource(clients.userConn), walletClient)
|
||||
roomTurnoverRewardSvc := roomturnoverrewardservice.New(repository, walletClient, roomClient)
|
||||
|
||||
// Tencent IM 是区域广播、房间礼物播报和红包播报的外部发布器。
|
||||
@ -102,7 +108,9 @@ func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository,
|
||||
sevenDayCheckIn: sevenDayCheckInSvc,
|
||||
growth: growthSvc,
|
||||
achievement: achievementSvc,
|
||||
agencyOpening: agencyOpeningSvc,
|
||||
weeklyStar: weeklyStarSvc,
|
||||
cpWeeklyRank: cpWeeklyRankSvc,
|
||||
roomTurnoverReward: roomTurnoverRewardSvc,
|
||||
broadcast: broadcastSvc,
|
||||
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) {
|
||||
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureAgencyOpeningTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureLuckyUserStateFlowColumns(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -84,6 +87,9 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
||||
if err := r.ensureInviteActivityRewardTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureCPWeeklyRankTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureTaskDefinitionMetadataColumns(ctx); err != nil {
|
||||
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"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
agencyopeningservice "hyapp/services/activity-service/internal/service/agencyopening"
|
||||
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
||||
growthservice "hyapp/services/activity-service/internal/service/growth"
|
||||
roomturnoverrewardservice "hyapp/services/activity-service/internal/service/roomturnoverreward"
|
||||
@ -25,20 +26,20 @@ type BroadcastServer struct {
|
||||
task *taskservice.Service
|
||||
roomReward *roomturnoverrewardservice.Service
|
||||
weeklyStar *weeklystarservice.Service
|
||||
agencyOpen *agencyopeningservice.Service
|
||||
}
|
||||
|
||||
// NewBroadcastServer 创建播报和 room event 消费共用的 gRPC adapter。
|
||||
// room-service 的 outbox 事件只投递一次,但 activity-service 内部有多条独立投影:
|
||||
// 播报负责 IM 展示,growth 负责等级进度,weekly-star 负责指定礼物榜单,roomReward 负责房间流水。
|
||||
// 这里把这些模块都挂到同一个 gRPC 入口,保证本地直连投递和 MQ 投递使用同一组业务副作用。
|
||||
func NewBroadcastServer(svc *broadcastservice.Service, growthSvc *growthservice.Service, weeklyStarSvc *weeklystarservice.Service, taskSvc *taskservice.Service, roomRewardSvc ...*roomturnoverrewardservice.Service) *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.growth = growthSvc
|
||||
server.weeklyStar = weeklyStarSvc
|
||||
server.task = taskSvc
|
||||
if len(roomRewardSvc) > 0 {
|
||||
server.roomReward = roomRewardSvc[0]
|
||||
}
|
||||
server.roomReward = roomRewardSvc
|
||||
server.agencyOpen = agencyOpeningSvc
|
||||
return server
|
||||
}
|
||||
|
||||
@ -129,6 +130,11 @@ func (s *BroadcastServer) ConsumeRoomEvent(ctx context.Context, req *activityv1.
|
||||
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 _, err := s.task.HandleRoomEvent(ctx, req.GetEnvelope()); err != nil {
|
||||
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/xerr"
|
||||
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"
|
||||
messageservice "hyapp/services/activity-service/internal/service/message"
|
||||
roomturnoverrewardservice "hyapp/services/activity-service/internal/service/roomturnoverreward"
|
||||
@ -23,17 +25,19 @@ type CronServer struct {
|
||||
achievement *achievementservice.Service
|
||||
weeklyStar *weeklystarservice.Service
|
||||
roomReward *roomturnoverrewardservice.Service
|
||||
agencyOpen *agencyopeningservice.Service
|
||||
cpWeekly *cpweeklyrankservice.Service
|
||||
}
|
||||
|
||||
// NewCronServer creates the internal cron adapter without exposing message storage.
|
||||
func NewCronServer(messageSvc *messageservice.Service, growthSvc *growthservice.Service, achievementSvc *achievementservice.Service, weeklyStarSvc *weeklystarservice.Service, roomRewardSvc ...*roomturnoverrewardservice.Service) *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.growth = growthSvc
|
||||
server.achievement = achievementSvc
|
||||
server.weeklyStar = weeklyStarSvc
|
||||
if len(roomRewardSvc) > 0 {
|
||||
server.roomReward = roomRewardSvc[0]
|
||||
}
|
||||
server.roomReward = roomRewardSvc
|
||||
server.agencyOpen = agencyOpeningSvc
|
||||
server.cpWeekly = cpWeeklyRankSvc
|
||||
return server
|
||||
}
|
||||
|
||||
@ -119,6 +123,25 @@ func (s *CronServer) ProcessWeeklyStarSettlementBatch(ctx context.Context, req *
|
||||
}, 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.
|
||||
func (s *CronServer) ProcessRoomTurnoverRewardSettlementBatch(ctx context.Context, req *activityv1.CronBatchRequest) (*activityv1.CronBatchResponse, error) {
|
||||
// cron-service 只负责调度,activity-service 才拥有上一 UTC 周期认领 settlement 和发奖状态机。
|
||||
@ -139,6 +162,25 @@ func (s *CronServer) ProcessRoomTurnoverRewardSettlementBatch(ctx context.Contex
|
||||
}, 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 {
|
||||
if value <= 0 {
|
||||
return 0
|
||||
|
||||
@ -50,6 +50,13 @@ tasks:
|
||||
lock_ttl: "1m"
|
||||
batch_size: 50
|
||||
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:
|
||||
enabled: true
|
||||
interval: "5m"
|
||||
|
||||
@ -50,6 +50,13 @@ tasks:
|
||||
lock_ttl: "1m"
|
||||
batch_size: 50
|
||||
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:
|
||||
enabled: true
|
||||
interval: "5m"
|
||||
|
||||
@ -50,6 +50,13 @@ tasks:
|
||||
lock_ttl: "1m"
|
||||
batch_size: 50
|
||||
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:
|
||||
enabled: true
|
||||
interval: "5m"
|
||||
|
||||
@ -123,6 +123,8 @@ func New(cfg config.Config) (*App, error) {
|
||||
"growth_level_reward": activityCron.ProcessLevelRewardBatch,
|
||||
"achievement_reward": activityCron.ProcessAchievementRewardBatch,
|
||||
"weekly_star_settlement": activityCron.ProcessWeeklyStarSettlementBatch,
|
||||
"agency_opening_settlement": activityCron.ProcessAgencyOpeningSettlementBatch,
|
||||
"cp_weekly_rank_settlement": activityCron.ProcessCPWeeklyRankSettlementBatch,
|
||||
"room_turnover_reward_settlement": activityCron.ProcessRoomTurnoverRewardSettlementBatch,
|
||||
"game_level_event_relay": gameCron.ProcessLevelEventOutboxBatch,
|
||||
"host_salary_daily_settlement": walletCron.ProcessHostSalaryDailySettlementBatch,
|
||||
|
||||
@ -172,6 +172,22 @@ func defaultTasks() map[string]TaskConfig {
|
||||
BatchSize: 50,
|
||||
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": {
|
||||
Enabled: true,
|
||||
Interval: "5m",
|
||||
|
||||
@ -54,6 +54,24 @@ func (c *ActivityCronClient) ProcessWeeklyStarSettlementBatch(ctx context.Contex
|
||||
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.
|
||||
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 自己处理幂等和状态推进。
|
||||
|
||||
@ -108,6 +108,8 @@ func New(cfg config.Config) (*App, error) {
|
||||
var luckyGiftClient client.LuckyGiftClient = client.NewGRPCLuckyGiftClient(activityConn)
|
||||
var roomTurnoverRewardClient client.RoomTurnoverRewardClient = client.NewGRPCRoomTurnoverRewardClient(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 gameClient client.GameClient = client.NewGRPCGameClient(gameConn)
|
||||
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.SetRoomTurnoverRewardClient(roomTurnoverRewardClient)
|
||||
handler.SetWeeklyStarClient(weeklyStarClient)
|
||||
handler.SetCPWeeklyRankClient(cpWeeklyRankClient)
|
||||
handler.SetAgencyOpeningClient(agencyOpeningClient)
|
||||
handler.SetBroadcastClient(broadcastClient)
|
||||
handler.SetGameClient(gameClient)
|
||||
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)
|
||||
CompleteOnboarding(ctx context.Context, req *userv1.CompleteOnboardingRequest) (*userv1.CompleteOnboardingResponse, 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)
|
||||
CreateManagerUserBlock(ctx context.Context, req *userv1.CreateManagerUserBlockRequest) (*userv1.CreateManagerUserBlockResponse, 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
roomTurnoverReward client.RoomTurnoverRewardClient
|
||||
weeklyStar client.WeeklyStarClient
|
||||
cpWeeklyRank client.CPWeeklyRankClient
|
||||
agencyOpening client.AgencyOpeningClient
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@ -43,6 +45,8 @@ type Config struct {
|
||||
LuckyGift client.LuckyGiftClient
|
||||
RoomTurnoverReward client.RoomTurnoverRewardClient
|
||||
WeeklyStar client.WeeklyStarClient
|
||||
CPWeeklyRank client.CPWeeklyRankClient
|
||||
AgencyOpening client.AgencyOpeningClient
|
||||
}
|
||||
|
||||
func New(config Config) *Handler {
|
||||
@ -62,6 +66,8 @@ func New(config Config) *Handler {
|
||||
luckyGift: config.LuckyGift,
|
||||
roomTurnoverReward: config.RoomTurnoverReward,
|
||||
weeklyStar: config.WeeklyStar,
|
||||
cpWeeklyRank: config.CPWeeklyRank,
|
||||
agencyOpening: config.AgencyOpening,
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,6 +89,10 @@ func (h *Handler) TaskHandlers() httproutes.TaskHandlers {
|
||||
GetWeeklyStarCurrent: h.getWeeklyStarCurrent,
|
||||
ListWeeklyStarLeaderboard: h.listWeeklyStarLeaderboard,
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
return &userv1.ChangeUserCountryResponse{}, nil
|
||||
}
|
||||
|
||||
@ -49,6 +49,8 @@ type Handler struct {
|
||||
luckyGift client.LuckyGiftClient
|
||||
roomTurnoverReward client.RoomTurnoverRewardClient
|
||||
weeklyStar client.WeeklyStarClient
|
||||
cpWeeklyRank client.CPWeeklyRankClient
|
||||
agencyOpening client.AgencyOpeningClient
|
||||
broadcastClient client.BroadcastClient
|
||||
gameClient client.GameClient
|
||||
statisticsClient client.StatisticsClient
|
||||
@ -265,6 +267,16 @@ func (h *Handler) SetWeeklyStarClient(weeklyStar client.WeeklyStarClient) {
|
||||
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。
|
||||
func (h *Handler) SetBroadcastClient(broadcastClient client.BroadcastClient) {
|
||||
h.broadcastClient = broadcastClient
|
||||
|
||||
@ -97,6 +97,7 @@ type UserHandlers struct {
|
||||
ProcessMyRoleInvitation http.HandlerFunc
|
||||
CompleteMyOnboarding http.HandlerFunc
|
||||
UpdateMyProfile http.HandlerFunc
|
||||
UpdateMyProfileBackground http.HandlerFunc
|
||||
ChangeMyCountry http.HandlerFunc
|
||||
ChangeMyDisplayUserID http.HandlerFunc
|
||||
ApplyMyPrettyDisplayUserID http.HandlerFunc
|
||||
@ -201,6 +202,10 @@ type TaskHandlers struct {
|
||||
GetWeeklyStarCurrent http.HandlerFunc
|
||||
ListWeeklyStarLeaderboard http.HandlerFunc
|
||||
ListWeeklyStarHistory http.HandlerFunc
|
||||
GetCPWeeklyRankStatus http.HandlerFunc
|
||||
GetAgencyOpeningStatus http.HandlerFunc
|
||||
ApplyAgencyOpening http.HandlerFunc
|
||||
ListAgencyOpeningLeaderboard http.HandlerFunc
|
||||
}
|
||||
|
||||
type LevelHandlers struct {
|
||||
@ -415,6 +420,7 @@ func (r routes) registerUserRoutes() {
|
||||
r.profile("/role-invitations/{invitation_id}/process", http.MethodPost, h.ProcessMyRoleInvitation)
|
||||
r.auth("/users/me/onboarding/complete", "", h.CompleteMyOnboarding)
|
||||
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.auth("/users/me/display-id/change", "", h.ChangeMyDisplayUserID)
|
||||
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/leaderboard", http.MethodGet, h.ListWeeklyStarLeaderboard)
|
||||
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 规则、抽奖和返奖实现。
|
||||
r.profile("/activities/lucky-gifts/check", http.MethodPost, h.CheckLuckyGift)
|
||||
r.profile("/tasks/tabs", http.MethodGet, h.ListTaskTabs)
|
||||
|
||||
@ -17,6 +17,16 @@ import (
|
||||
func TestGetMyOverviewComposesProfileWalletVIPStatsRolesAndEntries(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{
|
||||
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{
|
||||
UserId: 42,
|
||||
VisitorsCount: 1234,
|
||||
@ -74,7 +84,7 @@ func TestGetMyOverviewComposesProfileWalletVIPStatsRolesAndEntries(t *testing.T)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
stats := data["profile_stats"].(map[string]any)
|
||||
|
||||
@ -310,6 +310,7 @@ type fakeUserProfileClient struct {
|
||||
getRequests []*userv1.GetUserRequest
|
||||
lastComplete *userv1.CompleteOnboardingRequest
|
||||
lastUpdate *userv1.UpdateUserProfileRequest
|
||||
lastBackground *userv1.UpdateUserProfileBackgroundRequest
|
||||
lastCountry *userv1.ChangeUserCountryRequest
|
||||
lastCreateBlock *userv1.CreateManagerUserBlockRequest
|
||||
lastListBlocks *userv1.ListManagerUserBlocksRequest
|
||||
@ -830,6 +831,15 @@ func (f *fakeUserProfileClient) UpdateUserProfile(_ context.Context, req *userv1
|
||||
}}, 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) {
|
||||
f.lastCountry = req
|
||||
if f.countryErr != nil {
|
||||
@ -5391,7 +5401,7 @@ func TestSetPasswordRequiresTokenAndPropagatesActorUserID(t *testing.T) {
|
||||
func TestUpdateProfileUsesAuthenticatedUserID(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{}
|
||||
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.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
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" {
|
||||
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) {
|
||||
profileClient := &fakeUserProfileClient{}
|
||||
router := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient).Routes(auth.NewVerifier("secret"))
|
||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{42: {
|
||||
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.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
walletClient := &fakeWalletClient{h5OptionsResp: &walletv1.H5RechargeOptionsResponse{
|
||||
Products: []*walletv1.RechargeProduct{{
|
||||
|
||||
@ -84,6 +84,8 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
||||
LuckyGift: h.luckyGift,
|
||||
RoomTurnoverReward: h.roomTurnoverReward,
|
||||
WeeklyStar: h.weeklyStar,
|
||||
CPWeeklyRank: h.cpWeeklyRank,
|
||||
AgencyOpening: h.agencyOpening,
|
||||
})
|
||||
gameAPI := gameapi.New(gameapi.Config{
|
||||
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) {
|
||||
socialClient := &fakeUserSocialClient{}
|
||||
handler := NewHandler(&fakeRoomClient{})
|
||||
|
||||
@ -81,6 +81,7 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
||||
ProcessMyRoleInvitation: h.processMyRoleInvitation,
|
||||
CompleteMyOnboarding: h.completeMyOnboarding,
|
||||
UpdateMyProfile: h.updateMyProfile,
|
||||
UpdateMyProfileBackground: h.updateMyProfileBackground,
|
||||
ChangeMyCountry: h.changeMyCountry,
|
||||
ChangeMyDisplayUserID: h.changeMyDisplayUserID,
|
||||
ApplyMyPrettyDisplayUserID: h.applyMyPrettyDisplayUserID,
|
||||
|
||||
@ -192,7 +192,18 @@ func (h *Handler) recordProfileVisit(writer http.ResponseWriter, request *http.R
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
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) {
|
||||
|
||||
@ -62,6 +62,7 @@ type userProfileData struct {
|
||||
ProfileCompletedAtMs int64 `json:"profile_completed_at_ms"`
|
||||
OnboardingStatus string `json:"onboarding_status"`
|
||||
UpdatedAtMs int64 `json:"updated_at_ms"`
|
||||
ProfileBgImg string `json:"profile_bg_img"`
|
||||
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()))
|
||||
}
|
||||
|
||||
// updateMyProfile 修改当前用户的用户名、头像、性别和生日。
|
||||
// updateMyProfile 修改当前用户的用户名、头像、性别和生日;主页背景图走专用上传接口。
|
||||
func (h *Handler) updateMyProfile(writer http.ResponseWriter, request *http.Request) {
|
||||
var body struct {
|
||||
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))
|
||||
}
|
||||
|
||||
// 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 保证。
|
||||
func (h *Handler) changeMyCountry(writer http.ResponseWriter, request *http.Request) {
|
||||
var body struct {
|
||||
@ -589,6 +619,7 @@ func profileData(user *userv1.User, nextCountryChangeAtMs int64) userProfileData
|
||||
ProfileCompletedAtMs: user.GetProfileCompletedAtMs(),
|
||||
OnboardingStatus: user.GetOnboardingStatus(),
|
||||
UpdatedAtMs: user.GetUpdatedAtMs(),
|
||||
ProfileBgImg: user.GetProfileBgImg(),
|
||||
NextCountryChangeAtMs: nextCountryChangeAtMs,
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,7 +253,9 @@ func (h *Handler) confirmGooglePayment(writer http.ResponseWriter, request *http
|
||||
if purchaseTimeMS <= 0 {
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
register_device VARCHAR(128) NULL COMMENT '注册设备',
|
||||
os_version VARCHAR(64) NULL COMMENT 'os版本',
|
||||
avatar VARCHAR(512) NULL COMMENT '头像',
|
||||
profile_bg_img VARCHAR(512) NOT NULL DEFAULT '' COMMENT '个人主页信息卡背景图',
|
||||
birth_date DATE NULL COMMENT '出生日期',
|
||||
app_version 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',
|
||||
relationship_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '命中的 active 关系 ID',
|
||||
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',
|
||||
created_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),
|
||||
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 礼物事件消费位点表';
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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 亲密值排行榜分页读模型。
|
||||
type IntimacyLeaderboardPage struct {
|
||||
Items []IntimacyLeaderboardEntry
|
||||
|
||||
@ -256,6 +256,8 @@ type User struct {
|
||||
RegisterOSVersion string
|
||||
// Avatar 是注册入口传入的头像 HTTP(S) URL。
|
||||
Avatar string
|
||||
// ProfileBgImg 是用户主页信息卡背景图片 URL,属于用户主资料,不属于钱包装扮或 profile_card 资源。
|
||||
ProfileBgImg string
|
||||
// BirthDate 使用 yyyy-mm-dd 字符串表达生日,避免时区把日期偏移成前后一天。
|
||||
BirthDate string
|
||||
// RegisterAppVersion 是注册客户端版本。
|
||||
@ -628,6 +630,18 @@ type ProfileUpdateCommand struct {
|
||||
UpdatedAtMs int64
|
||||
}
|
||||
|
||||
// ProfileBackgroundUpdateCommand 描述用户个人信息页背景图的专用修改。
|
||||
type ProfileBackgroundUpdateCommand struct {
|
||||
// AppCode 是资料修改所属 App。
|
||||
AppCode string
|
||||
// UserID 是要修改背景图的用户,只能来自当前登录态。
|
||||
UserID int64
|
||||
// ProfileBgImg 是对象存储上传后返回的 HTTP(S) 背景图 URL。
|
||||
ProfileBgImg string
|
||||
// UpdatedAtMs 是本次修改时间。
|
||||
UpdatedAtMs int64
|
||||
}
|
||||
|
||||
// CompleteOnboardingCommand 描述注册页一次性补齐资料的事务输入。
|
||||
type CompleteOnboardingCommand struct {
|
||||
// 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)
|
||||
ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent, nowMs int64) (cpdomain.ConsumeResult, 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 读模型边界。
|
||||
@ -154,16 +155,31 @@ func (s *Service) CancelBreakRelationship(ctx context.Context, userID int64, rel
|
||||
// ConsumeGiftEvent 消费 room-service 已扣费并落房间状态的送礼事实。
|
||||
func (s *Service) ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent) (cpdomain.ConsumeResult, error) {
|
||||
event.CPRelationType = normalizeRelationType(event.CPRelationType)
|
||||
if event.CPRelationType == "" {
|
||||
return cpdomain.ConsumeResult{Consumed: false}, nil
|
||||
}
|
||||
// gift_value 是亲密值增量,沿用 room 热度结算后的值;0 值仍可创建申请,但不会增加已有关联亲密值。
|
||||
// gift_value 是亲密值和周榜分数增量,沿用 room-service 已扣费后的结算值;0 值仍可创建申请,但不会增加已有关联亲密值。
|
||||
if event.GiftValue < 0 {
|
||||
event.GiftValue = 0
|
||||
}
|
||||
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 服务读取。
|
||||
func (s *Service) RefreshIntimacyLeaderboard(ctx context.Context, limit int) (int, error) {
|
||||
// 榜单刷新是定时异步链路:任何核心依赖缺失都必须显式失败,让 cron run 记录失败状态,
|
||||
|
||||
@ -83,6 +83,10 @@ func (r *fakeLeaderboardRepo) ListActiveIntimacyLeaderboardEntries(context.Conte
|
||||
return r.entries, nil
|
||||
}
|
||||
|
||||
func (r *fakeLeaderboardRepo) ListWeeklyRankEntries(context.Context, string, int64, int64, int) ([]cpdomain.WeeklyRankEntry, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type fakeLeaderboardStore struct {
|
||||
entries []cpdomain.IntimacyLeaderboardEntry
|
||||
}
|
||||
|
||||
@ -107,6 +107,7 @@ type fakeModerationRepository struct {
|
||||
sessionIDs []string
|
||||
countrySessionIDs []string
|
||||
lastCommand UserStatusCommand
|
||||
lastProfileCommand userdomain.ProfileUpdateCommand
|
||||
lastCountryCommand userdomain.CountryChangeCommand
|
||||
countryChangeCalls int
|
||||
lastReport userdomain.Report
|
||||
@ -218,7 +219,13 @@ func (r *fakeModerationRepository) CreateUserWithIdentity(context.Context, userd
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -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 是注册页唯一提交入口。
|
||||
// 它必须一次性校验并写入 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) {
|
||||
|
||||
@ -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