Compare commits

...

10 Commits

Author SHA1 Message Date
zhx
11d2388449 Merge branch 'test' 2026-06-16 17:12:14 +08:00
zhx
15312f498b Merge branch 'test' of gitea.haiyihy.com:hy/hyapp-server into test 2026-06-16 17:00:09 +08:00
zhx
377443eec3 新增相关接口 2026-06-16 17:00:06 +08:00
zhx
d92509b5f2 fix: persist room rps config 2026-06-16 14:48:47 +08:00
zhx
cfe77431eb fix: avoid animated room rps gift icons 2026-06-16 14:15:35 +08:00
zhx
29b580f0ce fix: split room rps gift media urls 2026-06-16 14:11:07 +08:00
ZuoZuo
d455d24016 机器人房 2026-06-15 23:53:30 +08:00
zhx
604fcebda4 Merge branch 'test' of gitea.haiyihy.com:hy/hyapp-server into test 2026-06-15 19:17:11 +08:00
zhx
ad54880768 增加相关的支付和新app 2026-06-15 19:17:09 +08:00
zhx
55770ce23b fix(gateway): expose mic seat animation appearance 2026-06-15 15:02:28 +08:00
185 changed files with 20384 additions and 2687 deletions

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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"

View File

@ -1209,8 +1209,12 @@ type RoomGiftSent struct {
GiftAnimationUrl string `protobuf:"bytes,17,opt,name=gift_animation_url,json=giftAnimationUrl,proto3" json:"gift_animation_url,omitempty"`
TargetGiftValue int64 `protobuf:"varint,18,opt,name=target_gift_value,json=targetGiftValue,proto3" json:"target_gift_value,omitempty"`
GiftEffectTypes []string `protobuf:"bytes,19,rep,name=gift_effect_types,json=giftEffectTypes,proto3" json:"gift_effect_types,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// is_robot_gift 标记机器人房间展示用礼物事实;下游真实统计、任务和结算消费者必须跳过。
IsRobotGift bool `protobuf:"varint,20,opt,name=is_robot_gift,json=isRobotGift,proto3" json:"is_robot_gift,omitempty"`
// synthetic_lucky_gift 标记幸运礼物只用于房间表现,不写真实抽奖流水、不扣真实奖池、不发真实奖励。
SyntheticLuckyGift bool `protobuf:"varint,21,opt,name=synthetic_lucky_gift,json=syntheticLuckyGift,proto3" json:"synthetic_lucky_gift,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoomGiftSent) Reset() {
@ -1376,6 +1380,186 @@ func (x *RoomGiftSent) GetGiftEffectTypes() []string {
return nil
}
func (x *RoomGiftSent) GetIsRobotGift() bool {
if x != nil {
return x.IsRobotGift
}
return false
}
func (x *RoomGiftSent) GetSyntheticLuckyGift() bool {
if x != nil {
return x.SyntheticLuckyGift
}
return false
}
// RoomRobotLuckyGiftDrawn 是机器人房间专用幸运礼物展示事件。
// 它复用客户端 lucky_gift_drawn 表现协议,但不代表真实奖池抽奖和钱包返奖事实。
type RoomRobotLuckyGiftDrawn struct {
state protoimpl.MessageState `protogen:"open.v1"`
DrawId string `protobuf:"bytes,1,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"`
CommandId string `protobuf:"bytes,2,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
SenderUserId int64 `protobuf:"varint,3,opt,name=sender_user_id,json=senderUserId,proto3" json:"sender_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,4,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
GiftId string `protobuf:"bytes,5,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
GiftCount int32 `protobuf:"varint,6,opt,name=gift_count,json=giftCount,proto3" json:"gift_count,omitempty"`
PoolId string `protobuf:"bytes,7,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
MultiplierPpm int64 `protobuf:"varint,8,opt,name=multiplier_ppm,json=multiplierPpm,proto3" json:"multiplier_ppm,omitempty"`
BaseRewardCoins int64 `protobuf:"varint,9,opt,name=base_reward_coins,json=baseRewardCoins,proto3" json:"base_reward_coins,omitempty"`
RoomAtmosphereRewardCoins int64 `protobuf:"varint,10,opt,name=room_atmosphere_reward_coins,json=roomAtmosphereRewardCoins,proto3" json:"room_atmosphere_reward_coins,omitempty"`
ActivitySubsidyCoins int64 `protobuf:"varint,11,opt,name=activity_subsidy_coins,json=activitySubsidyCoins,proto3" json:"activity_subsidy_coins,omitempty"`
EffectiveRewardCoins int64 `protobuf:"varint,12,opt,name=effective_reward_coins,json=effectiveRewardCoins,proto3" json:"effective_reward_coins,omitempty"`
CreatedAtMs int64 `protobuf:"varint,13,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
VisibleRegionId int64 `protobuf:"varint,14,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"`
IsRobot bool `protobuf:"varint,15,opt,name=is_robot,json=isRobot,proto3" json:"is_robot,omitempty"`
Synthetic bool `protobuf:"varint,16,opt,name=synthetic,proto3" json:"synthetic,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoomRobotLuckyGiftDrawn) Reset() {
*x = RoomRobotLuckyGiftDrawn{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RoomRobotLuckyGiftDrawn) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RoomRobotLuckyGiftDrawn) ProtoMessage() {}
func (x *RoomRobotLuckyGiftDrawn) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RoomRobotLuckyGiftDrawn.ProtoReflect.Descriptor instead.
func (*RoomRobotLuckyGiftDrawn) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{17}
}
func (x *RoomRobotLuckyGiftDrawn) GetDrawId() string {
if x != nil {
return x.DrawId
}
return ""
}
func (x *RoomRobotLuckyGiftDrawn) GetCommandId() string {
if x != nil {
return x.CommandId
}
return ""
}
func (x *RoomRobotLuckyGiftDrawn) GetSenderUserId() int64 {
if x != nil {
return x.SenderUserId
}
return 0
}
func (x *RoomRobotLuckyGiftDrawn) GetTargetUserId() int64 {
if x != nil {
return x.TargetUserId
}
return 0
}
func (x *RoomRobotLuckyGiftDrawn) GetGiftId() string {
if x != nil {
return x.GiftId
}
return ""
}
func (x *RoomRobotLuckyGiftDrawn) GetGiftCount() int32 {
if x != nil {
return x.GiftCount
}
return 0
}
func (x *RoomRobotLuckyGiftDrawn) GetPoolId() string {
if x != nil {
return x.PoolId
}
return ""
}
func (x *RoomRobotLuckyGiftDrawn) GetMultiplierPpm() int64 {
if x != nil {
return x.MultiplierPpm
}
return 0
}
func (x *RoomRobotLuckyGiftDrawn) GetBaseRewardCoins() int64 {
if x != nil {
return x.BaseRewardCoins
}
return 0
}
func (x *RoomRobotLuckyGiftDrawn) GetRoomAtmosphereRewardCoins() int64 {
if x != nil {
return x.RoomAtmosphereRewardCoins
}
return 0
}
func (x *RoomRobotLuckyGiftDrawn) GetActivitySubsidyCoins() int64 {
if x != nil {
return x.ActivitySubsidyCoins
}
return 0
}
func (x *RoomRobotLuckyGiftDrawn) GetEffectiveRewardCoins() int64 {
if x != nil {
return x.EffectiveRewardCoins
}
return 0
}
func (x *RoomRobotLuckyGiftDrawn) GetCreatedAtMs() int64 {
if x != nil {
return x.CreatedAtMs
}
return 0
}
func (x *RoomRobotLuckyGiftDrawn) GetVisibleRegionId() int64 {
if x != nil {
return x.VisibleRegionId
}
return 0
}
func (x *RoomRobotLuckyGiftDrawn) GetIsRobot() bool {
if x != nil {
return x.IsRobot
}
return false
}
func (x *RoomRobotLuckyGiftDrawn) GetSynthetic() bool {
if x != nil {
return x.Synthetic
}
return false
}
// RoomHeatChanged 表达热度变化结果。
type RoomHeatChanged struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -1387,7 +1571,7 @@ type RoomHeatChanged struct {
func (x *RoomHeatChanged) Reset() {
*x = RoomHeatChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1399,7 +1583,7 @@ func (x *RoomHeatChanged) String() string {
func (*RoomHeatChanged) ProtoMessage() {}
func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1412,7 +1596,7 @@ func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomHeatChanged.ProtoReflect.Descriptor instead.
func (*RoomHeatChanged) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{17}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{18}
}
func (x *RoomHeatChanged) GetDelta() int64 {
@ -1441,7 +1625,7 @@ type RoomRankChanged struct {
func (x *RoomRankChanged) Reset() {
*x = RoomRankChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1453,7 +1637,7 @@ func (x *RoomRankChanged) String() string {
func (*RoomRankChanged) ProtoMessage() {}
func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1466,7 +1650,7 @@ func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomRankChanged.ProtoReflect.Descriptor instead.
func (*RoomRankChanged) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{18}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{19}
}
func (x *RoomRankChanged) GetUserId() int64 {
@ -1506,7 +1690,7 @@ type RoomRocketRewardGrant struct {
func (x *RoomRocketRewardGrant) Reset() {
*x = RoomRocketRewardGrant{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1518,7 +1702,7 @@ func (x *RoomRocketRewardGrant) String() string {
func (*RoomRocketRewardGrant) ProtoMessage() {}
func (x *RoomRocketRewardGrant) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1531,7 +1715,7 @@ func (x *RoomRocketRewardGrant) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomRocketRewardGrant.ProtoReflect.Descriptor instead.
func (*RoomRocketRewardGrant) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{19}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{20}
}
func (x *RoomRocketRewardGrant) GetRewardRole() string {
@ -1613,7 +1797,7 @@ type RoomRocketFuelChanged struct {
func (x *RoomRocketFuelChanged) Reset() {
*x = RoomRocketFuelChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1625,7 +1809,7 @@ func (x *RoomRocketFuelChanged) String() string {
func (*RoomRocketFuelChanged) ProtoMessage() {}
func (x *RoomRocketFuelChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1638,7 +1822,7 @@ func (x *RoomRocketFuelChanged) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomRocketFuelChanged.ProtoReflect.Descriptor instead.
func (*RoomRocketFuelChanged) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{20}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{21}
}
func (x *RoomRocketFuelChanged) GetRocketId() string {
@ -1762,7 +1946,7 @@ type RoomRocketIgnited struct {
func (x *RoomRocketIgnited) Reset() {
*x = RoomRocketIgnited{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1774,7 +1958,7 @@ func (x *RoomRocketIgnited) String() string {
func (*RoomRocketIgnited) ProtoMessage() {}
func (x *RoomRocketIgnited) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1787,7 +1971,7 @@ func (x *RoomRocketIgnited) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomRocketIgnited.ProtoReflect.Descriptor instead.
func (*RoomRocketIgnited) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{21}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{22}
}
func (x *RoomRocketIgnited) GetRocketId() string {
@ -1906,7 +2090,7 @@ type RoomRocketLaunched struct {
func (x *RoomRocketLaunched) Reset() {
*x = RoomRocketLaunched{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
mi := &file_proto_events_room_v1_events_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1918,7 +2102,7 @@ func (x *RoomRocketLaunched) String() string {
func (*RoomRocketLaunched) ProtoMessage() {}
func (x *RoomRocketLaunched) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
mi := &file_proto_events_room_v1_events_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1931,7 +2115,7 @@ func (x *RoomRocketLaunched) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomRocketLaunched.ProtoReflect.Descriptor instead.
func (*RoomRocketLaunched) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{22}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{23}
}
func (x *RoomRocketLaunched) GetRocketId() string {
@ -2009,7 +2193,7 @@ type RoomRocketRewardGranted struct {
func (x *RoomRocketRewardGranted) Reset() {
*x = RoomRocketRewardGranted{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[23]
mi := &file_proto_events_room_v1_events_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2021,7 +2205,7 @@ func (x *RoomRocketRewardGranted) String() string {
func (*RoomRocketRewardGranted) ProtoMessage() {}
func (x *RoomRocketRewardGranted) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[23]
mi := &file_proto_events_room_v1_events_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2034,7 +2218,7 @@ func (x *RoomRocketRewardGranted) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomRocketRewardGranted.ProtoReflect.Descriptor instead.
func (*RoomRocketRewardGranted) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{23}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{24}
}
func (x *RoomRocketRewardGranted) GetRocketId() string {
@ -2160,7 +2344,7 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\"\\\n" +
"\x10RoomUserUnbanned\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12$\n" +
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\"\xb5\x05\n" +
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\"\x8b\x06\n" +
"\fRoomGiftSent\x12$\n" +
"\x0esender_user_id\x18\x01 \x01(\x03R\fsenderUserId\x12$\n" +
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x17\n" +
@ -2186,7 +2370,29 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
"\rgift_icon_url\x18\x10 \x01(\tR\vgiftIconUrl\x12,\n" +
"\x12gift_animation_url\x18\x11 \x01(\tR\x10giftAnimationUrl\x12*\n" +
"\x11target_gift_value\x18\x12 \x01(\x03R\x0ftargetGiftValue\x12*\n" +
"\x11gift_effect_types\x18\x13 \x03(\tR\x0fgiftEffectTypes\"J\n" +
"\x11gift_effect_types\x18\x13 \x03(\tR\x0fgiftEffectTypes\x12\"\n" +
"\ris_robot_gift\x18\x14 \x01(\bR\visRobotGift\x120\n" +
"\x14synthetic_lucky_gift\x18\x15 \x01(\bR\x12syntheticLuckyGift\"\xf7\x04\n" +
"\x17RoomRobotLuckyGiftDrawn\x12\x17\n" +
"\adraw_id\x18\x01 \x01(\tR\x06drawId\x12\x1d\n" +
"\n" +
"command_id\x18\x02 \x01(\tR\tcommandId\x12$\n" +
"\x0esender_user_id\x18\x03 \x01(\x03R\fsenderUserId\x12$\n" +
"\x0etarget_user_id\x18\x04 \x01(\x03R\ftargetUserId\x12\x17\n" +
"\agift_id\x18\x05 \x01(\tR\x06giftId\x12\x1d\n" +
"\n" +
"gift_count\x18\x06 \x01(\x05R\tgiftCount\x12\x17\n" +
"\apool_id\x18\a \x01(\tR\x06poolId\x12%\n" +
"\x0emultiplier_ppm\x18\b \x01(\x03R\rmultiplierPpm\x12*\n" +
"\x11base_reward_coins\x18\t \x01(\x03R\x0fbaseRewardCoins\x12?\n" +
"\x1croom_atmosphere_reward_coins\x18\n" +
" \x01(\x03R\x19roomAtmosphereRewardCoins\x124\n" +
"\x16activity_subsidy_coins\x18\v \x01(\x03R\x14activitySubsidyCoins\x124\n" +
"\x16effective_reward_coins\x18\f \x01(\x03R\x14effectiveRewardCoins\x12\"\n" +
"\rcreated_at_ms\x18\r \x01(\x03R\vcreatedAtMs\x12*\n" +
"\x11visible_region_id\x18\x0e \x01(\x03R\x0fvisibleRegionId\x12\x19\n" +
"\bis_robot\x18\x0f \x01(\bR\aisRobot\x12\x1c\n" +
"\tsynthetic\x18\x10 \x01(\bR\tsynthetic\"J\n" +
"\x0fRoomHeatChanged\x12\x14\n" +
"\x05delta\x18\x01 \x01(\x03R\x05delta\x12!\n" +
"\fcurrent_heat\x18\x02 \x01(\x03R\vcurrentHeat\"_\n" +
@ -2272,7 +2478,7 @@ func file_proto_events_room_v1_events_proto_rawDescGZIP() []byte {
return file_proto_events_room_v1_events_proto_rawDescData
}
var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 24)
var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
var file_proto_events_room_v1_events_proto_goTypes = []any{
(*EventEnvelope)(nil), // 0: hyapp.events.room.v1.EventEnvelope
(*RoomCreated)(nil), // 1: hyapp.events.room.v1.RoomCreated
@ -2291,18 +2497,19 @@ var file_proto_events_room_v1_events_proto_goTypes = []any{
(*RoomUserKicked)(nil), // 14: hyapp.events.room.v1.RoomUserKicked
(*RoomUserUnbanned)(nil), // 15: hyapp.events.room.v1.RoomUserUnbanned
(*RoomGiftSent)(nil), // 16: hyapp.events.room.v1.RoomGiftSent
(*RoomHeatChanged)(nil), // 17: hyapp.events.room.v1.RoomHeatChanged
(*RoomRankChanged)(nil), // 18: hyapp.events.room.v1.RoomRankChanged
(*RoomRocketRewardGrant)(nil), // 19: hyapp.events.room.v1.RoomRocketRewardGrant
(*RoomRocketFuelChanged)(nil), // 20: hyapp.events.room.v1.RoomRocketFuelChanged
(*RoomRocketIgnited)(nil), // 21: hyapp.events.room.v1.RoomRocketIgnited
(*RoomRocketLaunched)(nil), // 22: hyapp.events.room.v1.RoomRocketLaunched
(*RoomRocketRewardGranted)(nil), // 23: hyapp.events.room.v1.RoomRocketRewardGranted
(*RoomRobotLuckyGiftDrawn)(nil), // 17: hyapp.events.room.v1.RoomRobotLuckyGiftDrawn
(*RoomHeatChanged)(nil), // 18: hyapp.events.room.v1.RoomHeatChanged
(*RoomRankChanged)(nil), // 19: hyapp.events.room.v1.RoomRankChanged
(*RoomRocketRewardGrant)(nil), // 20: hyapp.events.room.v1.RoomRocketRewardGrant
(*RoomRocketFuelChanged)(nil), // 21: hyapp.events.room.v1.RoomRocketFuelChanged
(*RoomRocketIgnited)(nil), // 22: hyapp.events.room.v1.RoomRocketIgnited
(*RoomRocketLaunched)(nil), // 23: hyapp.events.room.v1.RoomRocketLaunched
(*RoomRocketRewardGranted)(nil), // 24: hyapp.events.room.v1.RoomRocketRewardGranted
}
var file_proto_events_room_v1_events_proto_depIdxs = []int32{
4, // 0: hyapp.events.room.v1.RoomUserJoined.entry_vehicle:type_name -> hyapp.events.room.v1.RoomEntryVehicleSnapshot
19, // 1: hyapp.events.room.v1.RoomRocketLaunched.rewards:type_name -> hyapp.events.room.v1.RoomRocketRewardGrant
19, // 2: hyapp.events.room.v1.RoomRocketRewardGranted.rewards:type_name -> hyapp.events.room.v1.RoomRocketRewardGrant
20, // 1: hyapp.events.room.v1.RoomRocketLaunched.rewards:type_name -> hyapp.events.room.v1.RoomRocketRewardGrant
20, // 2: hyapp.events.room.v1.RoomRocketRewardGranted.rewards:type_name -> hyapp.events.room.v1.RoomRocketRewardGrant
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
@ -2321,7 +2528,7 @@ func file_proto_events_room_v1_events_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_events_room_v1_events_proto_rawDesc), len(file_proto_events_room_v1_events_proto_rawDesc)),
NumEnums: 0,
NumMessages: 24,
NumMessages: 25,
NumExtensions: 0,
NumServices: 0,
},

View File

@ -166,6 +166,31 @@ message RoomGiftSent {
string gift_animation_url = 17;
int64 target_gift_value = 18;
repeated string gift_effect_types = 19;
// is_robot_gift
bool is_robot_gift = 20;
// synthetic_lucky_gift
bool synthetic_lucky_gift = 21;
}
// RoomRobotLuckyGiftDrawn
// lucky_gift_drawn
message RoomRobotLuckyGiftDrawn {
string draw_id = 1;
string command_id = 2;
int64 sender_user_id = 3;
int64 target_user_id = 4;
string gift_id = 5;
int32 gift_count = 6;
string pool_id = 7;
int64 multiplier_ppm = 8;
int64 base_reward_coins = 9;
int64 room_atmosphere_reward_coins = 10;
int64 activity_subsidy_coins = 11;
int64 effective_reward_coins = 12;
int64 created_at_ms = 13;
int64 visible_region_id = 14;
bool is_robot = 15;
bool synthetic = 16;
}
// RoomHeatChanged

File diff suppressed because it is too large Load Diff

View File

@ -338,6 +338,85 @@ message AdminCancelRoomPinResponse {
int64 server_time_ms = 2;
}
message AdminRobotRoomGiftRule {
repeated string gift_ids = 1;
repeated string lucky_gift_ids = 2;
int64 normal_gift_interval_ms = 3;
int64 lucky_combo_min = 4;
int64 lucky_combo_max = 5;
int64 lucky_pause_min_ms = 6;
int64 lucky_pause_max_ms = 7;
}
message AdminRobotRoom {
string app_code = 1;
string room_id = 2;
string room_short_id = 3;
string title = 4;
string cover_url = 5;
int64 visible_region_id = 6;
string status = 7;
int64 owner_robot_user_id = 8;
repeated int64 robot_user_ids = 9;
AdminRobotRoomGiftRule gift_rule = 10;
uint64 created_by_admin_id = 11;
int64 created_at_ms = 12;
int64 updated_at_ms = 13;
}
message AdminListRobotRoomsRequest {
RequestMeta meta = 1;
int32 page = 2;
int32 page_size = 3;
string status = 4;
}
message AdminListRobotRoomsResponse {
repeated AdminRobotRoom rooms = 1;
int64 total = 2;
int64 server_time_ms = 3;
}
message AdminCreateRobotRoomRequest {
RequestMeta meta = 1;
int64 owner_robot_user_id = 2;
repeated int64 candidate_robot_user_ids = 3;
int32 min_robot_count = 4;
int32 max_robot_count = 5;
string room_name = 6;
string room_avatar = 7;
int64 visible_region_id = 8;
AdminRobotRoomGiftRule gift_rule = 9;
uint64 admin_id = 10;
}
message AdminCreateRobotRoomResponse {
AdminRobotRoom room = 1;
int64 server_time_ms = 2;
}
message AdminSetRobotRoomStatusRequest {
RequestMeta meta = 1;
string status = 2;
uint64 admin_id = 3;
}
message AdminSetRobotRoomStatusResponse {
AdminRobotRoom room = 1;
int64 server_time_ms = 2;
}
message AdminFilterAvailableRoomRobotsRequest {
RequestMeta meta = 1;
repeated int64 user_ids = 2;
}
message AdminFilterAvailableRoomRobotsResponse {
repeated int64 available_user_ids = 1;
repeated int64 occupied_user_ids = 2;
int64 server_time_ms = 3;
}
// RoomBanState ban
message RoomBanState {
int64 user_id = 1;
@ -440,6 +519,10 @@ message CreateRoomRequest {
string room_short_id = 8;
// owner_country_code gateway user-service
string owner_country_code = 9;
// robot_room gateway
bool robot_room = 10;
// robot_user_ids
repeated int64 robot_user_ids = 11;
}
// CreateRoomResponse
@ -1149,6 +1232,8 @@ service RoomCommandService {
rpc AdminUpdateRoomSeatConfig(AdminUpdateRoomSeatConfigRequest) returns (AdminUpdateRoomSeatConfigResponse);
rpc AdminCreateRoomPin(AdminCreateRoomPinRequest) returns (AdminCreateRoomPinResponse);
rpc AdminCancelRoomPin(AdminCancelRoomPinRequest) returns (AdminCancelRoomPinResponse);
rpc AdminCreateRobotRoom(AdminCreateRobotRoomRequest) returns (AdminCreateRobotRoomResponse);
rpc AdminSetRobotRoomStatus(AdminSetRobotRoomStatusRequest) returns (AdminSetRobotRoomStatusResponse);
rpc MicUp(MicUpRequest) returns (MicUpResponse);
rpc MicDown(MicDownRequest) returns (MicDownResponse);
rpc ChangeMicSeat(ChangeMicSeatRequest) returns (ChangeMicSeatResponse);
@ -1182,6 +1267,8 @@ service RoomQueryService {
rpc AdminGetRoomRocketConfig(AdminGetRoomRocketConfigRequest) returns (AdminGetRoomRocketConfigResponse);
rpc AdminGetRoomSeatConfig(AdminGetRoomSeatConfigRequest) returns (AdminGetRoomSeatConfigResponse);
rpc AdminListRoomPins(AdminListRoomPinsRequest) returns (AdminListRoomPinsResponse);
rpc AdminListRobotRooms(AdminListRobotRoomsRequest) returns (AdminListRobotRoomsResponse);
rpc AdminFilterAvailableRoomRobots(AdminFilterAvailableRoomRobotsRequest) returns (AdminFilterAvailableRoomRobotsResponse);
rpc ListRooms(ListRoomsRequest) returns (ListRoomsResponse);
rpc ListRoomFeeds(ListRoomFeedsRequest) returns (ListRoomsResponse);
rpc ListRoomGiftLeaderboard(ListRoomGiftLeaderboardRequest) returns (ListRoomGiftLeaderboardResponse);

View File

@ -33,6 +33,8 @@ const (
RoomCommandService_AdminUpdateRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoomSeatConfig"
RoomCommandService_AdminCreateRoomPin_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCreateRoomPin"
RoomCommandService_AdminCancelRoomPin_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCancelRoomPin"
RoomCommandService_AdminCreateRobotRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCreateRobotRoom"
RoomCommandService_AdminSetRobotRoomStatus_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminSetRobotRoomStatus"
RoomCommandService_MicUp_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicUp"
RoomCommandService_MicDown_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicDown"
RoomCommandService_ChangeMicSeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/ChangeMicSeat"
@ -73,6 +75,8 @@ type RoomCommandServiceClient interface {
AdminUpdateRoomSeatConfig(ctx context.Context, in *AdminUpdateRoomSeatConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomSeatConfigResponse, error)
AdminCreateRoomPin(ctx context.Context, in *AdminCreateRoomPinRequest, opts ...grpc.CallOption) (*AdminCreateRoomPinResponse, error)
AdminCancelRoomPin(ctx context.Context, in *AdminCancelRoomPinRequest, opts ...grpc.CallOption) (*AdminCancelRoomPinResponse, error)
AdminCreateRobotRoom(ctx context.Context, in *AdminCreateRobotRoomRequest, opts ...grpc.CallOption) (*AdminCreateRobotRoomResponse, error)
AdminSetRobotRoomStatus(ctx context.Context, in *AdminSetRobotRoomStatusRequest, opts ...grpc.CallOption) (*AdminSetRobotRoomStatusResponse, error)
MicUp(ctx context.Context, in *MicUpRequest, opts ...grpc.CallOption) (*MicUpResponse, error)
MicDown(ctx context.Context, in *MicDownRequest, opts ...grpc.CallOption) (*MicDownResponse, error)
ChangeMicSeat(ctx context.Context, in *ChangeMicSeatRequest, opts ...grpc.CallOption) (*ChangeMicSeatResponse, error)
@ -241,6 +245,26 @@ func (c *roomCommandServiceClient) AdminCancelRoomPin(ctx context.Context, in *A
return out, nil
}
func (c *roomCommandServiceClient) AdminCreateRobotRoom(ctx context.Context, in *AdminCreateRobotRoomRequest, opts ...grpc.CallOption) (*AdminCreateRobotRoomResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminCreateRobotRoomResponse)
err := c.cc.Invoke(ctx, RoomCommandService_AdminCreateRobotRoom_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomCommandServiceClient) AdminSetRobotRoomStatus(ctx context.Context, in *AdminSetRobotRoomStatusRequest, opts ...grpc.CallOption) (*AdminSetRobotRoomStatusResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminSetRobotRoomStatusResponse)
err := c.cc.Invoke(ctx, RoomCommandService_AdminSetRobotRoomStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomCommandServiceClient) MicUp(ctx context.Context, in *MicUpRequest, opts ...grpc.CallOption) (*MicUpResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MicUpResponse)
@ -441,6 +465,8 @@ type RoomCommandServiceServer interface {
AdminUpdateRoomSeatConfig(context.Context, *AdminUpdateRoomSeatConfigRequest) (*AdminUpdateRoomSeatConfigResponse, error)
AdminCreateRoomPin(context.Context, *AdminCreateRoomPinRequest) (*AdminCreateRoomPinResponse, error)
AdminCancelRoomPin(context.Context, *AdminCancelRoomPinRequest) (*AdminCancelRoomPinResponse, error)
AdminCreateRobotRoom(context.Context, *AdminCreateRobotRoomRequest) (*AdminCreateRobotRoomResponse, error)
AdminSetRobotRoomStatus(context.Context, *AdminSetRobotRoomStatusRequest) (*AdminSetRobotRoomStatusResponse, error)
MicUp(context.Context, *MicUpRequest) (*MicUpResponse, error)
MicDown(context.Context, *MicDownRequest) (*MicDownResponse, error)
ChangeMicSeat(context.Context, *ChangeMicSeatRequest) (*ChangeMicSeatResponse, error)
@ -511,6 +537,12 @@ func (UnimplementedRoomCommandServiceServer) AdminCreateRoomPin(context.Context,
func (UnimplementedRoomCommandServiceServer) AdminCancelRoomPin(context.Context, *AdminCancelRoomPinRequest) (*AdminCancelRoomPinResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminCancelRoomPin not implemented")
}
func (UnimplementedRoomCommandServiceServer) AdminCreateRobotRoom(context.Context, *AdminCreateRobotRoomRequest) (*AdminCreateRobotRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminCreateRobotRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) AdminSetRobotRoomStatus(context.Context, *AdminSetRobotRoomStatusRequest) (*AdminSetRobotRoomStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminSetRobotRoomStatus not implemented")
}
func (UnimplementedRoomCommandServiceServer) MicUp(context.Context, *MicUpRequest) (*MicUpResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MicUp not implemented")
}
@ -838,6 +870,42 @@ func _RoomCommandService_AdminCancelRoomPin_Handler(srv interface{}, ctx context
return interceptor(ctx, in, info, handler)
}
func _RoomCommandService_AdminCreateRobotRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminCreateRobotRoomRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomCommandServiceServer).AdminCreateRobotRoom(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomCommandService_AdminCreateRobotRoom_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomCommandServiceServer).AdminCreateRobotRoom(ctx, req.(*AdminCreateRobotRoomRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomCommandService_AdminSetRobotRoomStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminSetRobotRoomStatusRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomCommandServiceServer).AdminSetRobotRoomStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomCommandService_AdminSetRobotRoomStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomCommandServiceServer).AdminSetRobotRoomStatus(ctx, req.(*AdminSetRobotRoomStatusRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomCommandService_MicUp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MicUpRequest)
if err := dec(in); err != nil {
@ -1225,6 +1293,14 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
MethodName: "AdminCancelRoomPin",
Handler: _RoomCommandService_AdminCancelRoomPin_Handler,
},
{
MethodName: "AdminCreateRobotRoom",
Handler: _RoomCommandService_AdminCreateRobotRoom_Handler,
},
{
MethodName: "AdminSetRobotRoomStatus",
Handler: _RoomCommandService_AdminSetRobotRoomStatus_Handler,
},
{
MethodName: "MicUp",
Handler: _RoomCommandService_MicUp_Handler,
@ -1447,21 +1523,23 @@ var RoomGuardService_ServiceDesc = grpc.ServiceDesc{
}
const (
RoomQueryService_AdminListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRooms"
RoomQueryService_AdminGetRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoom"
RoomQueryService_AdminGetRoomRocketConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomRocketConfig"
RoomQueryService_AdminGetRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomSeatConfig"
RoomQueryService_AdminListRoomPins_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRoomPins"
RoomQueryService_ListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRooms"
RoomQueryService_ListRoomFeeds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomFeeds"
RoomQueryService_ListRoomGiftLeaderboard_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomGiftLeaderboard"
RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom"
RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom"
RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot"
RoomQueryService_ListRoomBackgrounds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBackgrounds"
RoomQueryService_GetRoomRocket_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomRocket"
RoomQueryService_ListRoomOnlineUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomOnlineUsers"
RoomQueryService_ListRoomBannedUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBannedUsers"
RoomQueryService_AdminListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRooms"
RoomQueryService_AdminGetRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoom"
RoomQueryService_AdminGetRoomRocketConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomRocketConfig"
RoomQueryService_AdminGetRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomSeatConfig"
RoomQueryService_AdminListRoomPins_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRoomPins"
RoomQueryService_AdminListRobotRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRobotRooms"
RoomQueryService_AdminFilterAvailableRoomRobots_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminFilterAvailableRoomRobots"
RoomQueryService_ListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRooms"
RoomQueryService_ListRoomFeeds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomFeeds"
RoomQueryService_ListRoomGiftLeaderboard_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomGiftLeaderboard"
RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom"
RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom"
RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot"
RoomQueryService_ListRoomBackgrounds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBackgrounds"
RoomQueryService_GetRoomRocket_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomRocket"
RoomQueryService_ListRoomOnlineUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomOnlineUsers"
RoomQueryService_ListRoomBannedUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBannedUsers"
)
// RoomQueryServiceClient is the client API for RoomQueryService service.
@ -1475,6 +1553,8 @@ type RoomQueryServiceClient interface {
AdminGetRoomRocketConfig(ctx context.Context, in *AdminGetRoomRocketConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomRocketConfigResponse, error)
AdminGetRoomSeatConfig(ctx context.Context, in *AdminGetRoomSeatConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomSeatConfigResponse, error)
AdminListRoomPins(ctx context.Context, in *AdminListRoomPinsRequest, opts ...grpc.CallOption) (*AdminListRoomPinsResponse, error)
AdminListRobotRooms(ctx context.Context, in *AdminListRobotRoomsRequest, opts ...grpc.CallOption) (*AdminListRobotRoomsResponse, error)
AdminFilterAvailableRoomRobots(ctx context.Context, in *AdminFilterAvailableRoomRobotsRequest, opts ...grpc.CallOption) (*AdminFilterAvailableRoomRobotsResponse, error)
ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error)
ListRoomFeeds(ctx context.Context, in *ListRoomFeedsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error)
ListRoomGiftLeaderboard(ctx context.Context, in *ListRoomGiftLeaderboardRequest, opts ...grpc.CallOption) (*ListRoomGiftLeaderboardResponse, error)
@ -1545,6 +1625,26 @@ func (c *roomQueryServiceClient) AdminListRoomPins(ctx context.Context, in *Admi
return out, nil
}
func (c *roomQueryServiceClient) AdminListRobotRooms(ctx context.Context, in *AdminListRobotRoomsRequest, opts ...grpc.CallOption) (*AdminListRobotRoomsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminListRobotRoomsResponse)
err := c.cc.Invoke(ctx, RoomQueryService_AdminListRobotRooms_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomQueryServiceClient) AdminFilterAvailableRoomRobots(ctx context.Context, in *AdminFilterAvailableRoomRobotsRequest, opts ...grpc.CallOption) (*AdminFilterAvailableRoomRobotsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminFilterAvailableRoomRobotsResponse)
err := c.cc.Invoke(ctx, RoomQueryService_AdminFilterAvailableRoomRobots_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomQueryServiceClient) ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListRoomsResponse)
@ -1656,6 +1756,8 @@ type RoomQueryServiceServer interface {
AdminGetRoomRocketConfig(context.Context, *AdminGetRoomRocketConfigRequest) (*AdminGetRoomRocketConfigResponse, error)
AdminGetRoomSeatConfig(context.Context, *AdminGetRoomSeatConfigRequest) (*AdminGetRoomSeatConfigResponse, error)
AdminListRoomPins(context.Context, *AdminListRoomPinsRequest) (*AdminListRoomPinsResponse, error)
AdminListRobotRooms(context.Context, *AdminListRobotRoomsRequest) (*AdminListRobotRoomsResponse, error)
AdminFilterAvailableRoomRobots(context.Context, *AdminFilterAvailableRoomRobotsRequest) (*AdminFilterAvailableRoomRobotsResponse, error)
ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error)
ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error)
ListRoomGiftLeaderboard(context.Context, *ListRoomGiftLeaderboardRequest) (*ListRoomGiftLeaderboardResponse, error)
@ -1691,6 +1793,12 @@ func (UnimplementedRoomQueryServiceServer) AdminGetRoomSeatConfig(context.Contex
func (UnimplementedRoomQueryServiceServer) AdminListRoomPins(context.Context, *AdminListRoomPinsRequest) (*AdminListRoomPinsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminListRoomPins not implemented")
}
func (UnimplementedRoomQueryServiceServer) AdminListRobotRooms(context.Context, *AdminListRobotRoomsRequest) (*AdminListRobotRoomsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminListRobotRooms not implemented")
}
func (UnimplementedRoomQueryServiceServer) AdminFilterAvailableRoomRobots(context.Context, *AdminFilterAvailableRoomRobotsRequest) (*AdminFilterAvailableRoomRobotsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminFilterAvailableRoomRobots not implemented")
}
func (UnimplementedRoomQueryServiceServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRooms not implemented")
}
@ -1832,6 +1940,42 @@ func _RoomQueryService_AdminListRoomPins_Handler(srv interface{}, ctx context.Co
return interceptor(ctx, in, info, handler)
}
func _RoomQueryService_AdminListRobotRooms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminListRobotRoomsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomQueryServiceServer).AdminListRobotRooms(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomQueryService_AdminListRobotRooms_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).AdminListRobotRooms(ctx, req.(*AdminListRobotRoomsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomQueryService_AdminFilterAvailableRoomRobots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminFilterAvailableRoomRobotsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomQueryServiceServer).AdminFilterAvailableRoomRobots(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomQueryService_AdminFilterAvailableRoomRobots_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).AdminFilterAvailableRoomRobots(ctx, req.(*AdminFilterAvailableRoomRobotsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomQueryService_ListRooms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRoomsRequest)
if err := dec(in); err != nil {
@ -2039,6 +2183,14 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
MethodName: "AdminListRoomPins",
Handler: _RoomQueryService_AdminListRoomPins_Handler,
},
{
MethodName: "AdminListRobotRooms",
Handler: _RoomQueryService_AdminListRobotRooms_Handler,
},
{
MethodName: "AdminFilterAvailableRoomRobots",
Handler: _RoomQueryService_AdminFilterAvailableRoomRobots_Handler,
},
{
MethodName: "ListRooms",
Handler: _RoomQueryService_ListRooms_Handler,

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -91,6 +91,21 @@ message BatchDebitGiftResponse {
repeated BatchDebitGiftReceipt receipts = 2;
}
// DebitRobotGiftRequest
// ROBOT_COIN
message DebitRobotGiftRequest {
string command_id = 1;
string room_id = 2;
int64 sender_user_id = 3;
int64 target_user_id = 4;
string gift_id = 5;
int32 gift_count = 6;
string price_version = 7;
string app_code = 8;
int64 region_id = 9;
int64 sender_region_id = 10;
}
// AssetBalance
message AssetBalance {
string asset_type = 1;
@ -1276,6 +1291,10 @@ message CreateH5RechargeOrderRequest {
string notify_url = 12;
string client_ip = 13;
string language = 14;
// payer_name / payer_account gateway H5
string payer_name = 15;
string payer_account = 16;
string payer_email = 17;
}
message SubmitH5RechargeTxRequest {
@ -1591,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;
@ -1825,6 +1866,7 @@ service WalletCronService {
service WalletService {
rpc DebitGift(DebitGiftRequest) returns (DebitGiftResponse);
rpc BatchDebitGift(BatchDebitGiftRequest) returns (BatchDebitGiftResponse);
rpc DebitRobotGift(DebitRobotGiftRequest) returns (DebitGiftResponse);
rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse);
rpc GetActiveHostSalaryPolicy(GetActiveHostSalaryPolicyRequest) returns (GetActiveHostSalaryPolicyResponse);
rpc GetHostSalaryProgress(GetHostSalaryProgressRequest) returns (GetHostSalaryProgressResponse);
@ -1893,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);

View File

@ -203,6 +203,7 @@ var WalletCronService_ServiceDesc = grpc.ServiceDesc{
const (
WalletService_DebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitGift"
WalletService_BatchDebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchDebitGift"
WalletService_DebitRobotGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitRobotGift"
WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances"
WalletService_GetActiveHostSalaryPolicy_FullMethodName = "/hyapp.wallet.v1.WalletService/GetActiveHostSalaryPolicy"
WalletService_GetHostSalaryProgress_FullMethodName = "/hyapp.wallet.v1.WalletService/GetHostSalaryProgress"
@ -271,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"
@ -290,6 +292,7 @@ const (
type WalletServiceClient interface {
DebitGift(ctx context.Context, in *DebitGiftRequest, opts ...grpc.CallOption) (*DebitGiftResponse, error)
BatchDebitGift(ctx context.Context, in *BatchDebitGiftRequest, opts ...grpc.CallOption) (*BatchDebitGiftResponse, error)
DebitRobotGift(ctx context.Context, in *DebitRobotGiftRequest, opts ...grpc.CallOption) (*DebitGiftResponse, error)
GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error)
GetActiveHostSalaryPolicy(ctx context.Context, in *GetActiveHostSalaryPolicyRequest, opts ...grpc.CallOption) (*GetActiveHostSalaryPolicyResponse, error)
GetHostSalaryProgress(ctx context.Context, in *GetHostSalaryProgressRequest, opts ...grpc.CallOption) (*GetHostSalaryProgressResponse, error)
@ -358,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)
@ -397,6 +401,16 @@ func (c *walletServiceClient) BatchDebitGift(ctx context.Context, in *BatchDebit
return out, nil
}
func (c *walletServiceClient) DebitRobotGift(ctx context.Context, in *DebitRobotGiftRequest, opts ...grpc.CallOption) (*DebitGiftResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DebitGiftResponse)
err := c.cc.Invoke(ctx, WalletService_DebitRobotGift_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetBalancesResponse)
@ -1077,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)
@ -1175,6 +1199,7 @@ func (c *walletServiceClient) RetryRedPacketRefund(ctx context.Context, in *Retr
type WalletServiceServer interface {
DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error)
BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error)
DebitRobotGift(context.Context, *DebitRobotGiftRequest) (*DebitGiftResponse, error)
GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error)
GetActiveHostSalaryPolicy(context.Context, *GetActiveHostSalaryPolicyRequest) (*GetActiveHostSalaryPolicyResponse, error)
GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error)
@ -1243,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)
@ -1268,6 +1294,9 @@ func (UnimplementedWalletServiceServer) DebitGift(context.Context, *DebitGiftReq
func (UnimplementedWalletServiceServer) BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error) {
return nil, status.Error(codes.Unimplemented, "method BatchDebitGift not implemented")
}
func (UnimplementedWalletServiceServer) DebitRobotGift(context.Context, *DebitRobotGiftRequest) (*DebitGiftResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DebitRobotGift not implemented")
}
func (UnimplementedWalletServiceServer) GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetBalances not implemented")
}
@ -1472,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")
}
@ -1556,6 +1588,24 @@ func _WalletService_BatchDebitGift_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler)
}
func _WalletService_DebitRobotGift_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DebitRobotGiftRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).DebitRobotGift(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_DebitRobotGift_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).DebitRobotGift(ctx, req.(*DebitRobotGiftRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_GetBalances_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetBalancesRequest)
if err := dec(in); err != nil {
@ -2780,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 {
@ -2957,6 +3025,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
MethodName: "BatchDebitGift",
Handler: _WalletService_BatchDebitGift_Handler,
},
{
MethodName: "DebitRobotGift",
Handler: _WalletService_DebitRobotGift_Handler,
},
{
MethodName: "GetBalances",
Handler: _WalletService_GetBalances_Handler,
@ -3229,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,

View 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不触发背包或装扮刷新。

View File

@ -225,6 +225,7 @@ Body
| 权益校验 | 必须是当前登录用户自己的有效权益;过期、禁用、剩余数量为 0 都不能佩戴。 |
| 发放默认佩戴 | `badge``mic_seat_animation` 发放成功后默认佩戴。 |
| 徽章展示 | `badge` 是拥有即佩戴的多装备资源;资料页多徽章展示仍使用 `/api/v1/badges/me``/api/v1/badges/display`。 |
| 资料卡背景 | `profile_card` 只用于装扮资料卡;个人主页背景图使用用户资料字段 `profile_bg_img`,不走背包佩戴。 |
常见错误:

View File

@ -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:

View File

@ -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:

View File

@ -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 状态 |

View File

@ -0,0 +1,98 @@
# 机器人房间产品文档
## 背景
语音房冷启动阶段,房间列表和新房首屏容易显得冷清。机器人房间用于在内部可控范围内营造房间活跃氛围:机器人账号创建房间、进入房间、静音上麦,并在房间内相互送礼,触发客户端已有的礼物动效和幸运礼物展示。
机器人房间不面向真人主播发放收益,不参与真实抽奖、真实奖池、榜单和结算。
## 产品目标
- 后台可创建和控制机器人房间。
- 每个机器人房间自动拉取一批未被占用的机器人账号进房。
- 机器人可静音上麦,让房间麦位呈现活跃状态。
- 机器人之间自动送普通礼物和幸运礼物。
- 幸运礼物展示复用客户端现有飘屏/房间展示协议,但不影响真实幸运礼物奖池和流水。
- 机器人房间积分和麦位热度正常增长,用于房间内氛围展示。
- 机器人礼物不产生真人收益,不污染充值、消费、结算和排行榜数据。
## 使用入口
后台管理系统路径:
`房间管理 / 机器人房间`
权限:
- `room-robot:view`:查看机器人房间。
- `room-robot:create`:创建机器人房间。
- `room-robot:update`:启动/停止机器人房间。
## 创建机器人房间
点击 `增加机器人房间` 后配置:
- 房间名称:机器人房间对外展示名称。
- 房主机器人:从未被机器人房间占用的房间机器人账号中选择。
- 候选机器人:从未被机器人房间占用的机器人账号中选择,默认可选择全部可用机器人。
- 进房人数范围:例如 `6-10`,系统会从候选机器人中随机抽取一个人数。
- 礼物合集:普通礼物池,机器人普通送礼时随机选择。
- 幸运礼物合集:幸运礼物展示池,机器人幸运送礼时随机选择。
- 普通礼物频次:每个机器人每隔固定毫秒数送一次普通礼物。
- 幸运礼物连击范围:每轮幸运礼物随机生成连击次数。
- 幸运礼物间隔范围:一轮幸运礼物结束后,随机等待一段时间再开始下一轮。
## 送礼规则
### 普通礼物
- 每个机器人独立执行。
- 到达配置间隔后,从普通礼物合集中随机选择 1 个礼物。
- 随机选择同房间另一个机器人作为收礼人。
- 每次只送 1 个目标机器人。
### 幸运礼物
- 每个机器人独立执行。
- 从幸运礼物合集中随机选择 1 个礼物。
- 当前机器人向同房间所有其他机器人送礼。
- 按配置范围随机生成连击次数,例如 `100-10000`
- 连击完成后,按配置间隔范围随机等待,例如 `5-20 秒`,再开始下一轮。
## 状态管理
机器人房间支持:
- 运行中:机器人送礼循环运行。
- 已停止:机器人送礼循环停止,配置和房间占用保留。
后台列表中可使用状态开关快速启动或停止。
## 数据隔离
机器人房间遵循以下隔离规则:
- 机器人只给同房间机器人送礼。
- 服务端禁止机器人给真人送礼。
- 机器人礼物使用机器人专用金币 `ROBOT_COIN`
- 不消耗真人金币 `COIN`
- 不产生主播钻石和工资周期钻石。
- 不写真实礼物墙。
- 不参与真实幸运礼物奖池、抽奖、返奖和统计。
- 不参与房间排行榜、活动排行榜和结算。
## 运营建议
- 单房机器人数量建议从 `6-10` 开始。
- 普通礼物频次建议不要过低,避免房间消息过密。
- 幸运礼物连击范围建议根据活动氛围配置,测试环境可用较小范围验证。
- 礼物合集应选择低价或专门用于展示的礼物,避免用户误解真实返奖。
- 机器人房间应优先用于冷启动、活动预热和列表氛围补足。
## 风险与限制
- 机器人房间不应和真人主播收益挂钩。
- 不建议使用真人主播账号作为机器人账号。
- 如果候选机器人不足,创建会失败。
- 同一个机器人不能同时被多个 active 机器人房间占用。
- 停止房间只停止送礼循环,不删除房间配置。

View File

@ -0,0 +1,243 @@
# 机器人房间技术架构文档
## 目标
机器人房间提供后台配置驱动的内部房间编排能力,支持机器人创建房间、进房、虚拟静音上麦、普通礼物展示、幸运礼物展示,并保证与真实钱包、真实幸运礼物、真实奖池、真实榜单和结算隔离。
## 模块边界
### admin-platform
- 提供机器人房间配置页面。
- 调用 admin-server HTTP 接口。
- 不直接访问业务服务。
### admin-server
- 暴露后台 HTTP API。
- 做权限、审计、参数校验和 DTO 转换。
- 通过 gRPC client 调用 room-service 和 robot-service。
- 通过 wallet-service gRPC 获取礼物配置。
- 只读用户库补齐机器人展示资料。
### robot-service
- 只拥有机器人账号池事实。
- 房间机器人池表:`hyapp_robot.robot_room_robots`
- 不处理进房、麦位、送礼和 IM。
### room-service
- 机器人房间配置 owner。
- 创建真实房间状态。
- 管理机器人房间配置表:`hyapp_room.room_robot_rooms`
- 执行机器人进房、虚拟静音上麦、送礼循环。
- 强校验机器人送礼仅限同房间机器人。
- 生成机器人幸运礼物展示事件。
### wallet-service
- 机器人礼物扣费入口:`DebitRobotGift`
- 使用专用资产 `ROBOT_COIN`
- 自动补足机器人专用金币后扣减。
- 不发出真实 `WalletGiftDebited` 事件。
- 不写主播周期钻石和真实礼物墙。
### activity/statistics
- 消费房间送礼事件时跳过 `is_robot_gift`
- 机器人礼物不进入真实活动、榜单、任务、成长值和统计。
## 核心数据表
### `hyapp_robot.robot_room_robots`
机器人账号池事实表。
关键字段:
- `app_code`
- `room_scene`
- `user_id`
- `status`
- `last_used_at_ms`
- `used_count`
### `hyapp_room.room_robot_rooms`
机器人房间配置和占用事实表。
关键字段:
- `room_id`
- `room_short_id`
- `status`
- `owner_robot_user_id`
- `robot_user_ids_json`
- `gift_ids_json`
- `lucky_gift_ids_json`
- `normal_gift_interval_ms`
- `lucky_combo_min`
- `lucky_combo_max`
- `lucky_pause_min_ms`
- `lucky_pause_max_ms`
## 后台 API
后台 HTTP
- `GET /api/v1/admin/rooms/robot-rooms`
- `GET /api/v1/admin/rooms/robot-rooms/available-robots`
- `POST /api/v1/admin/rooms/robot-rooms`
- `POST /api/v1/admin/rooms/robot-rooms/{room_id}/start`
- `POST /api/v1/admin/rooms/robot-rooms/{room_id}/stop`
room-service gRPC
- `AdminListRobotRooms`
- `AdminFilterAvailableRoomRobots`
- `AdminCreateRobotRoom`
- `AdminSetRobotRoomStatus`
wallet-service gRPC
- `DebitRobotGift`
## 创建流程
```mermaid
sequenceDiagram
participant Admin as admin-platform
participant AS as admin-server
participant RS as room-service
participant RB as robot-service
participant DB as MySQL
Admin->>AS: POST /admin/rooms/robot-rooms
AS->>RS: AdminCreateRobotRoom
RS->>DB: 查询 active 机器人房占用
RS->>RS: 从候选机器人随机抽取人数
RS->>RS: CreateRoom(robot_room=true)
RS->>RS: JoinRoom for robots
RS->>RS: RobotVirtualMicUp for robots
RS->>DB: 写 room_robot_rooms
RS->>RS: start runtime loops
RS-->>AS: AdminRobotRoom
AS-->>Admin: 机器人房间详情
```
## 运行时循环
room-service 启动时会扫描所有 active 机器人房间并恢复运行时循环。
每个 active 机器人房间:
- 为每个机器人启动普通礼物 loop。
- 为每个机器人启动幸运礼物 loop。
- stop 后取消对应房间的 context。
普通礼物 loop
1. 等待 `normal_gift_interval_ms`
2. 从 `gift_ids_json` 随机选择礼物。
3. 从同房间其他机器人随机选择 1 个目标。
4. 调用 `RobotSendGift`
幸运礼物 loop
1. 从 `lucky_gift_ids_json` 随机选择礼物。
2. 随机生成 `lucky_combo_min-lucky_combo_max` 连击次数。
3. 当前机器人向同房间其他机器人逐个送礼。
4. 使用 `synthetic_lucky_gift=true`,跳过真实抽奖。
5. 等待 `lucky_pause_min_ms-lucky_pause_max_ms` 后继续。
## 服务端安全校验
room-service 在机器人送礼时强校验:
- 当前房间必须是机器人房间。
- sender 必须在该机器人房间 `robot_user_ids_json` 内。
- target 必须在同一机器人房间 `robot_user_ids_json` 内。
- sender 不能给真人送礼。
- 当前机器人礼物只允许单目标调用。
## 钱包隔离
机器人礼物使用 `DebitRobotGift`
- 资产类型固定为 `ROBOT_COIN`
- 如果机器人 `ROBOT_COIN` 不足,同一事务内自动补足。
- 立即扣减本次礼物价格。
- 交易类型为 `robot_gift_debit`
- 不写真实 `gift_debit`
- 不发 `WalletGiftDebited`
- 不写 `user_gift_wall`
- 不写 `host_period_diamond_accounts`
## 幸运礼物隔离
机器人幸运礼物不会调用真实幸运抽奖链路。
room-service 只生成展示事件:
- `RoomRobotLuckyGiftDrawn`
- 对 IM 客户端映射为 `lucky_gift_drawn`
- 标记 `synthetic=true`
- 携带机器人房间、送礼人、收礼人、礼物和展示金额字段
真实奖池、真实流水、真实返奖、真实统计完全不写入。
## 排行榜与统计隔离
`RoomGiftSent` 增加:
- `is_robot_gift`
- `synthetic_lucky_gift`
下游服务看到 `is_robot_gift=true` 时跳过:
- 活动播报统计
- 周星
- 房间流水奖励
- 每日任务
- 成长值
- statistics 礼物消费与幸运礼物统计
room-service 本地仍允许机器人房内:
- 房间热度增长
- 麦位热度增长
- 房内展示事件发送
## 本地接口测试
测试工具:
```bash
go run ./tools/robot-room-api-test
```
默认依赖:
- admin-server: `http://127.0.0.1:13100/api/v1`
- MySQL: `127.0.0.1:23306`
- 管理员账号: `admin / admin123`
工具会:
1. 种 12 个房间机器人账号。
2. 种 2 个礼物配置和价格。
3. 登录 admin-server。
4. 调用可用机器人列表接口。
5. 创建机器人房间。
6. 查询 active 列表。
7. 停止、启动、再次停止机器人房间。
8. 校验数据库中机器人房间配置和真实 `gift_debit` 隔离。
## 运维注意事项
- 多实例 room-service 下,机器人房间 runtime 是进程内循环;重启会从 MySQL active 配置恢复。
- 机器人房间配置以 MySQL 为准。
- 同一机器人账号不能被多个 active 机器人房间占用。
- 停止机器人房间只停止 runtime不删除房间。
- 如果需要彻底释放机器人账号,应增加删除/归档能力后再开放。

View File

@ -62,6 +62,18 @@ GET /api/v1/users/me/appearance
"entitlement_id": "ent-vehicle",
"expires_at_ms": 1999999999999
},
"mic_seat_animation": {
"resource_id": 8106,
"resource_code": "mic_seat_wave_gold",
"resource_type": "mic_seat_animation",
"name": "Gold Mic Wave",
"asset_url": "https://cdn.example/mic-seat.png",
"preview_url": "",
"animation_url": "https://cdn.example/mic-seat.svga",
"metadata_json": "{}",
"entitlement_id": "ent-mic-seat-wave",
"expires_at_ms": 1999999999999
},
"equipped_badges": [],
"badges": [
{
@ -94,7 +106,20 @@ GET /api/v1/users/me/appearance
"honor_badges": [],
"server_time_ms": 1778256000000
},
"resources": [],
"resources": [
{
"resource_id": 8106,
"resource_code": "mic_seat_wave_gold",
"resource_type": "mic_seat_animation",
"name": "Gold Mic Wave",
"asset_url": "https://cdn.example/mic-seat.png",
"preview_url": "",
"animation_url": "https://cdn.example/mic-seat.svga",
"metadata_json": "{}",
"entitlement_id": "ent-mic-seat-wave",
"expires_at_ms": 1999999999999
}
],
"server_time_ms": 1778256000000
}
```
@ -107,6 +132,7 @@ GET /api/v1/users/me/appearance
| avatar_frame | 当前佩戴头像框,未佩戴时为 `{}` |
| profile_card | 当前佩戴背景卡,未佩戴时为 `{}` |
| vehicle | 当前佩戴入场座驾,未佩戴时为 `{}` |
| mic_seat_animation | 当前佩戴麦位声波,未佩戴时为 `{}` |
| equipped_badges | wallet 当前佩戴的 badge 权益 |
| badges | 房间、公屏等高频展示用徽章,取 `strip_badges` |
| badge_profile | 完整徽章展示配置 |
@ -116,7 +142,7 @@ GET /api/v1/users/me/appearance
说明:
- 过期佩戴不会返回。
- 头像框、背景卡、入场座驾来自 wallet 当前佩戴。
- 头像框、背景卡、入场座驾、麦位声波来自 wallet 当前佩戴。
- 徽章展示配置来自 activity。
- 客户端渲染动画优先使用 `animation_url`,没有动画时用 `asset_url``preview_url` 兜底。
@ -345,6 +371,18 @@ GET /api/v1/users/room-display-profiles:batch?user_ids=42,43
"entitlement_id": "ent-vehicle",
"expires_at_ms": 1999999999999
},
"mic_seat_animation": {
"resource_id": 8106,
"resource_code": "mic_seat_wave_gold",
"resource_type": "mic_seat_animation",
"name": "Gold Mic Wave",
"asset_url": "https://cdn.example/mic-seat.png",
"preview_url": "",
"animation_url": "https://cdn.example/mic-seat.svga",
"metadata_json": "{}",
"entitlement_id": "ent-mic-seat-wave",
"expires_at_ms": 1999999999999
},
"charm": 0
}
]
@ -356,6 +394,7 @@ GET /api/v1/users/room-display-profiles:batch?user_ids=42,43
- 房间、公屏、在线列表展示头像框用 `avatar_frame`
- 房间资料卡展示用 `profile_card`,从 `metadata_json.profile_card_layout` 解析 `content_top``content_height`
- 入场座驾展示用 `vehicle`
- 麦位声波展示用 `mic_seat_animation`
- 徽章条展示用 `badges`
- 过期佩戴不会返回。
@ -484,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` 播放入场座驾。

View File

@ -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

View File

@ -78,7 +78,9 @@ func codeFromGRPC(code codes.Code) Code {
return Unauthorized
case codes.PermissionDenied:
return PermissionDenied
case codes.Unavailable:
case codes.Unavailable, codes.DeadlineExceeded:
// 下游服务未启动、网络不可达或 gRPC deadline 到期都属于依赖暂不可用gateway 应返回 UPSTREAM_ERROR
// 不能降级为 INTERNAL_ERROR否则本地联调和线上排障都会误判成业务代码内部崩溃。
return Unavailable
case codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted:
return Conflict

View File

@ -127,3 +127,66 @@ INSERT IGNORE INTO third_party_payment_methods (
('lalu', 'mifapay', 'PH', 'Philippines', 'PHP', 'Ewallet', 'Gcash', 'GCash', '', 'active', 1, 1310, @now_ms, @now_ms),
('lalu', 'mifapay', 'PH', 'Philippines', 'PHP', 'Ewallet', 'PayMaya', 'PayMaya', '', 'active', 1, 1320, @now_ms, @now_ms),
('lalu', 'mifapay', 'PH', 'Philippines', 'PHP', 'QR', 'QRPH', 'QRPH', '', 'active', 1, 1330, @now_ms, @now_ms);
-- MiFaPay logo 使用已上传 COS 的固定对象地址;仅覆盖空值、早期占位图和 Apifox 临时预览图,避免误改运营手工替换的正式图。
UPDATE third_party_payment_methods m
JOIN (
SELECT 'AE' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ae/card-creditcard.png' AS logo_url
UNION ALL SELECT 'BD' AS country_code, 'Ewallet' AS pay_way, 'Nagad' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/bd/ewallet-nagad.png' AS logo_url
UNION ALL SELECT 'BD' AS country_code, 'Ewallet' AS pay_way, 'bKash' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/bd/ewallet-bkash.png' AS logo_url
UNION ALL SELECT 'BH' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/bh/card-creditcard.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/card-creditcard.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'FawryPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-fawrypay.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Meeza' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-meeza.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Opay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-opay.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Orange' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-orange.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Vodafone' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-vodafone.png' AS logo_url
UNION ALL SELECT 'GLOBAL' AS country_code, 'BankTransfer' AS pay_way, 'UnionPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/banktransfer-unionpay.png' AS logo_url
UNION ALL SELECT 'GLOBAL' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/card-creditcard.png' AS logo_url
UNION ALL SELECT 'GLOBAL' AS country_code, 'Ewallet' AS pay_way, 'ApplePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/ewallet-applepay.png' AS logo_url
UNION ALL SELECT 'GLOBAL' AS country_code, 'Ewallet' AS pay_way, 'GooglePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/ewallet-googlepay.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'BCA' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-bca.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'BNI' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-bni.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'BRI' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-bri.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'Mandiri' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-mandiri.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'Permata' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-permata.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'QRIS' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-qris.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'DANA' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-dana.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'LinkAja' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-linkaja.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'OVO' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-ovo.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'ShopeePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-shopeepay.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'QR' AS pay_way, 'QRIS' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/qr-qris.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'BHIM' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-bhim.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'GooglePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-googlepay.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'Mobikwik' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-mobikwik.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'Paytm' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-paytm.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'PhonePe' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-phonepe.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'UPI' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-upi.png' AS logo_url
UNION ALL SELECT 'KW' AS country_code, 'Card' AS pay_way, 'KENT' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/kw/card-kent.png' AS logo_url
UNION ALL SELECT 'KW' AS country_code, 'Card' AS pay_way, 'Knet' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/kw/card-knet.png' AS logo_url
UNION ALL SELECT 'MY' AS country_code, 'BankTransfer' AS pay_way, 'FPX' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/my/banktransfer-fpx.png' AS logo_url
UNION ALL SELECT 'MY' AS country_code, 'Ewallet' AS pay_way, 'TNG' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/my/ewallet-tng.png' AS logo_url
UNION ALL SELECT 'OM' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/om/card-creditcard.png' AS logo_url
UNION ALL SELECT 'PH' AS country_code, 'BankTransfer' AS pay_way, 'QRPH' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/banktransfer-qrph.png' AS logo_url
UNION ALL SELECT 'PH' AS country_code, 'Ewallet' AS pay_way, 'Gcash' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/ewallet-gcash.png' AS logo_url
UNION ALL SELECT 'PH' AS country_code, 'Ewallet' AS pay_way, 'PayMaya' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/ewallet-paymaya.png' AS logo_url
UNION ALL SELECT 'PH' AS country_code, 'QR' AS pay_way, 'QRPH' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/qr-qrph.png' AS logo_url
UNION ALL SELECT 'PK' AS country_code, 'Ewallet' AS pay_way, 'Easypaisa' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/pk/ewallet-easypaisa.png' AS logo_url
UNION ALL SELECT 'PK' AS country_code, 'Ewallet' AS pay_way, 'JazzCash' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/pk/ewallet-jazzcash.png' AS logo_url
UNION ALL SELECT 'QA' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/qa/card-creditcard.png' AS logo_url
UNION ALL SELECT 'SA' AS country_code, 'Card' AS pay_way, 'MADA' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/sa/card-mada.png' AS logo_url
UNION ALL SELECT 'SA' AS country_code, 'Ewallet' AS pay_way, 'ApplePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/sa/ewallet-applepay.png' AS logo_url
UNION ALL SELECT 'SA' AS country_code, 'Ewallet' AS pay_way, 'STCPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/sa/ewallet-stcpay.png' AS logo_url
UNION ALL SELECT 'TH' AS country_code, 'BankTransfer' AS pay_way, 'PromptPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/th/banktransfer-promptpay.png' AS logo_url
UNION ALL SELECT 'TH' AS country_code, 'Ewallet' AS pay_way, 'TrueMoney' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/th/ewallet-truemoney.png' AS logo_url
UNION ALL SELECT 'VN' AS country_code, 'BankTransfer' AS pay_way, 'VN_BankTransfer' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/vn/banktransfer-vn-banktransfer.png' AS logo_url
UNION ALL SELECT 'VN' AS country_code, 'QR' AS pay_way, 'VietQR' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/vn/qr-vietqr.png' AS logo_url
) logos
ON UPPER(m.country_code) = logos.country_code
AND LOWER(m.pay_way) = LOWER(logos.pay_way)
AND LOWER(m.pay_type) = LOWER(logos.pay_type)
SET m.logo_url = logos.logo_url,
m.updated_at_ms = @now_ms
WHERE m.app_code = 'lalu'
AND m.provider_code = 'mifapay'
AND (m.logo_url = '' OR m.logo_url LIKE 'https://placehold.co/%' OR m.logo_url LIKE '%/image-preview');

View File

@ -105,3 +105,66 @@ ON DUPLICATE KEY UPDATE
usd_to_currency_rate = IF(usd_to_currency_rate > 0, usd_to_currency_rate, VALUES(usd_to_currency_rate)),
sort_order = VALUES(sort_order),
updated_at_ms = VALUES(updated_at_ms);
-- MiFaPay logo 使用已上传 COS 的固定对象地址;仅覆盖空值、早期占位图和 Apifox 临时预览图,避免误改运营手工替换的正式图。
UPDATE third_party_payment_methods m
JOIN (
SELECT 'AE' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ae/card-creditcard.png' AS logo_url
UNION ALL SELECT 'BD' AS country_code, 'Ewallet' AS pay_way, 'Nagad' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/bd/ewallet-nagad.png' AS logo_url
UNION ALL SELECT 'BD' AS country_code, 'Ewallet' AS pay_way, 'bKash' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/bd/ewallet-bkash.png' AS logo_url
UNION ALL SELECT 'BH' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/bh/card-creditcard.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/card-creditcard.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'FawryPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-fawrypay.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Meeza' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-meeza.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Opay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-opay.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Orange' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-orange.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Vodafone' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-vodafone.png' AS logo_url
UNION ALL SELECT 'GLOBAL' AS country_code, 'BankTransfer' AS pay_way, 'UnionPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/banktransfer-unionpay.png' AS logo_url
UNION ALL SELECT 'GLOBAL' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/card-creditcard.png' AS logo_url
UNION ALL SELECT 'GLOBAL' AS country_code, 'Ewallet' AS pay_way, 'ApplePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/ewallet-applepay.png' AS logo_url
UNION ALL SELECT 'GLOBAL' AS country_code, 'Ewallet' AS pay_way, 'GooglePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/ewallet-googlepay.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'BCA' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-bca.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'BNI' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-bni.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'BRI' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-bri.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'Mandiri' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-mandiri.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'Permata' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-permata.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'QRIS' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-qris.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'DANA' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-dana.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'LinkAja' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-linkaja.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'OVO' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-ovo.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'ShopeePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-shopeepay.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'QR' AS pay_way, 'QRIS' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/qr-qris.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'BHIM' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-bhim.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'GooglePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-googlepay.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'Mobikwik' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-mobikwik.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'Paytm' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-paytm.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'PhonePe' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-phonepe.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'UPI' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-upi.png' AS logo_url
UNION ALL SELECT 'KW' AS country_code, 'Card' AS pay_way, 'KENT' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/kw/card-kent.png' AS logo_url
UNION ALL SELECT 'KW' AS country_code, 'Card' AS pay_way, 'Knet' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/kw/card-knet.png' AS logo_url
UNION ALL SELECT 'MY' AS country_code, 'BankTransfer' AS pay_way, 'FPX' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/my/banktransfer-fpx.png' AS logo_url
UNION ALL SELECT 'MY' AS country_code, 'Ewallet' AS pay_way, 'TNG' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/my/ewallet-tng.png' AS logo_url
UNION ALL SELECT 'OM' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/om/card-creditcard.png' AS logo_url
UNION ALL SELECT 'PH' AS country_code, 'BankTransfer' AS pay_way, 'QRPH' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/banktransfer-qrph.png' AS logo_url
UNION ALL SELECT 'PH' AS country_code, 'Ewallet' AS pay_way, 'Gcash' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/ewallet-gcash.png' AS logo_url
UNION ALL SELECT 'PH' AS country_code, 'Ewallet' AS pay_way, 'PayMaya' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/ewallet-paymaya.png' AS logo_url
UNION ALL SELECT 'PH' AS country_code, 'QR' AS pay_way, 'QRPH' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/qr-qrph.png' AS logo_url
UNION ALL SELECT 'PK' AS country_code, 'Ewallet' AS pay_way, 'Easypaisa' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/pk/ewallet-easypaisa.png' AS logo_url
UNION ALL SELECT 'PK' AS country_code, 'Ewallet' AS pay_way, 'JazzCash' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/pk/ewallet-jazzcash.png' AS logo_url
UNION ALL SELECT 'QA' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/qa/card-creditcard.png' AS logo_url
UNION ALL SELECT 'SA' AS country_code, 'Card' AS pay_way, 'MADA' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/sa/card-mada.png' AS logo_url
UNION ALL SELECT 'SA' AS country_code, 'Ewallet' AS pay_way, 'ApplePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/sa/ewallet-applepay.png' AS logo_url
UNION ALL SELECT 'SA' AS country_code, 'Ewallet' AS pay_way, 'STCPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/sa/ewallet-stcpay.png' AS logo_url
UNION ALL SELECT 'TH' AS country_code, 'BankTransfer' AS pay_way, 'PromptPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/th/banktransfer-promptpay.png' AS logo_url
UNION ALL SELECT 'TH' AS country_code, 'Ewallet' AS pay_way, 'TrueMoney' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/th/ewallet-truemoney.png' AS logo_url
UNION ALL SELECT 'VN' AS country_code, 'BankTransfer' AS pay_way, 'VN_BankTransfer' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/vn/banktransfer-vn-banktransfer.png' AS logo_url
UNION ALL SELECT 'VN' AS country_code, 'QR' AS pay_way, 'VietQR' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/vn/qr-vietqr.png' AS logo_url
) logos
ON UPPER(m.country_code) = logos.country_code
AND LOWER(m.pay_way) = LOWER(logos.pay_way)
AND LOWER(m.pay_type) = LOWER(logos.pay_type)
SET m.logo_url = logos.logo_url,
m.updated_at_ms = @now_ms
WHERE m.app_code = 'lalu'
AND m.provider_code = 'mifapay'
AND (m.logo_url = '' OR m.logo_url LIKE 'https://placehold.co/%' OR m.logo_url LIKE '%/image-preview');

View File

@ -0,0 +1,44 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
USE hyapp_user;
CREATE TABLE IF NOT EXISTS manager_profiles (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
user_id BIGINT NOT NULL PRIMARY KEY COMMENT '经理用户 ID',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
contact_info VARCHAR(128) NOT NULL DEFAULT '' COMMENT '联系信息',
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台管理员 ID',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
KEY idx_manager_profiles_status (app_code, status, updated_at_ms),
KEY idx_manager_profiles_created_by (app_code, created_by_admin_id, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='经理资料表';
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'manager_profiles' AND COLUMN_NAME = 'contact_info') = 0,
'ALTER TABLE manager_profiles ADD COLUMN contact_info VARCHAR(128) NOT NULL DEFAULT '''' COMMENT ''联系信息'' AFTER status',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
INSERT INTO manager_profiles (
app_code, user_id, status, contact_info, created_by_admin_id, created_at_ms, updated_at_ms
)
SELECT
bl.app_code,
bl.created_by_user_id,
'active',
'',
0,
MIN(bl.created_at_ms),
MAX(bl.updated_at_ms)
FROM bd_leader_profiles bl
JOIN users u
ON u.app_code = bl.app_code
AND u.user_id = bl.created_by_user_id
WHERE bl.created_by_user_id > 0
GROUP BY bl.app_code, bl.created_by_user_id
ON DUPLICATE KEY UPDATE
updated_at_ms = GREATEST(manager_profiles.updated_at_ms, VALUES(updated_at_ms));

View File

@ -0,0 +1,66 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
-- MiFaPay logo 使用已上传 COS 的固定对象地址;仅覆盖空值、早期占位图和 Apifox 临时预览图,避免误改运营手工替换的正式图。
UPDATE third_party_payment_methods m
JOIN (
SELECT 'AE' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ae/card-creditcard.png' AS logo_url
UNION ALL SELECT 'BD' AS country_code, 'Ewallet' AS pay_way, 'Nagad' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/bd/ewallet-nagad.png' AS logo_url
UNION ALL SELECT 'BD' AS country_code, 'Ewallet' AS pay_way, 'bKash' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/bd/ewallet-bkash.png' AS logo_url
UNION ALL SELECT 'BH' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/bh/card-creditcard.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/card-creditcard.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'FawryPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-fawrypay.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Meeza' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-meeza.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Opay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-opay.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Orange' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-orange.png' AS logo_url
UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Vodafone' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-vodafone.png' AS logo_url
UNION ALL SELECT 'GLOBAL' AS country_code, 'BankTransfer' AS pay_way, 'UnionPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/banktransfer-unionpay.png' AS logo_url
UNION ALL SELECT 'GLOBAL' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/card-creditcard.png' AS logo_url
UNION ALL SELECT 'GLOBAL' AS country_code, 'Ewallet' AS pay_way, 'ApplePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/ewallet-applepay.png' AS logo_url
UNION ALL SELECT 'GLOBAL' AS country_code, 'Ewallet' AS pay_way, 'GooglePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/ewallet-googlepay.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'BCA' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-bca.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'BNI' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-bni.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'BRI' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-bri.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'Mandiri' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-mandiri.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'Permata' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-permata.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'QRIS' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-qris.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'DANA' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-dana.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'LinkAja' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-linkaja.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'OVO' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-ovo.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'ShopeePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-shopeepay.png' AS logo_url
UNION ALL SELECT 'ID' AS country_code, 'QR' AS pay_way, 'QRIS' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/qr-qris.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'BHIM' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-bhim.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'GooglePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-googlepay.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'Mobikwik' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-mobikwik.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'Paytm' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-paytm.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'PhonePe' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-phonepe.png' AS logo_url
UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'UPI' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-upi.png' AS logo_url
UNION ALL SELECT 'KW' AS country_code, 'Card' AS pay_way, 'KENT' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/kw/card-kent.png' AS logo_url
UNION ALL SELECT 'KW' AS country_code, 'Card' AS pay_way, 'Knet' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/kw/card-knet.png' AS logo_url
UNION ALL SELECT 'MY' AS country_code, 'BankTransfer' AS pay_way, 'FPX' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/my/banktransfer-fpx.png' AS logo_url
UNION ALL SELECT 'MY' AS country_code, 'Ewallet' AS pay_way, 'TNG' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/my/ewallet-tng.png' AS logo_url
UNION ALL SELECT 'OM' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/om/card-creditcard.png' AS logo_url
UNION ALL SELECT 'PH' AS country_code, 'BankTransfer' AS pay_way, 'QRPH' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/banktransfer-qrph.png' AS logo_url
UNION ALL SELECT 'PH' AS country_code, 'Ewallet' AS pay_way, 'Gcash' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/ewallet-gcash.png' AS logo_url
UNION ALL SELECT 'PH' AS country_code, 'Ewallet' AS pay_way, 'PayMaya' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/ewallet-paymaya.png' AS logo_url
UNION ALL SELECT 'PH' AS country_code, 'QR' AS pay_way, 'QRPH' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/qr-qrph.png' AS logo_url
UNION ALL SELECT 'PK' AS country_code, 'Ewallet' AS pay_way, 'Easypaisa' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/pk/ewallet-easypaisa.png' AS logo_url
UNION ALL SELECT 'PK' AS country_code, 'Ewallet' AS pay_way, 'JazzCash' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/pk/ewallet-jazzcash.png' AS logo_url
UNION ALL SELECT 'QA' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/qa/card-creditcard.png' AS logo_url
UNION ALL SELECT 'SA' AS country_code, 'Card' AS pay_way, 'MADA' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/sa/card-mada.png' AS logo_url
UNION ALL SELECT 'SA' AS country_code, 'Ewallet' AS pay_way, 'ApplePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/sa/ewallet-applepay.png' AS logo_url
UNION ALL SELECT 'SA' AS country_code, 'Ewallet' AS pay_way, 'STCPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/sa/ewallet-stcpay.png' AS logo_url
UNION ALL SELECT 'TH' AS country_code, 'BankTransfer' AS pay_way, 'PromptPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/th/banktransfer-promptpay.png' AS logo_url
UNION ALL SELECT 'TH' AS country_code, 'Ewallet' AS pay_way, 'TrueMoney' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/th/ewallet-truemoney.png' AS logo_url
UNION ALL SELECT 'VN' AS country_code, 'BankTransfer' AS pay_way, 'VN_BankTransfer' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/vn/banktransfer-vn-banktransfer.png' AS logo_url
UNION ALL SELECT 'VN' AS country_code, 'QR' AS pay_way, 'VietQR' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/vn/qr-vietqr.png' AS logo_url
) logos
ON UPPER(m.country_code) = logos.country_code
AND LOWER(m.pay_way) = LOWER(logos.pay_way)
AND LOWER(m.pay_type) = LOWER(logos.pay_type)
SET m.logo_url = logos.logo_url,
m.updated_at_ms = @now_ms
WHERE m.app_code = 'lalu'
AND m.provider_code = 'mifapay'
AND (m.logo_url = '' OR m.logo_url LIKE 'https://placehold.co/%' OR m.logo_url LIKE '%/image-preview');

View File

@ -0,0 +1,15 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
USE hyapp_user;
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
-- App 注册表是 gateway 包名解析的事实来源Huwaa 使用独立 app_code 隔离用户、钱包、房间和统计数据。
INSERT INTO apps (app_code, app_name, package_name, platform, status, created_at_ms, updated_at_ms)
VALUES ('huwaa', 'Huwaa', 'com.app.huwaa', '', 'active', @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
app_name = VALUES(app_name),
package_name = VALUES(package_name),
platform = VALUES(platform),
status = VALUES(status),
updated_at_ms = VALUES(updated_at_ms);

View File

@ -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 一致。
## 验证

View File

@ -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`
相关 IMFlutter 消息页的系统/活动标签读取后端 inbox当前客户端只展示标题、摘要/正文和时间。腾讯云 IM 单聊和房间群消息不走这个后台入口。
## Verify
```bash

View File

@ -18,6 +18,7 @@ import (
"hyapp-admin-server/internal/config"
"hyapp-admin-server/internal/integration/activityclient"
"hyapp-admin-server/internal/integration/gameclient"
"hyapp-admin-server/internal/integration/robotclient"
"hyapp-admin-server/internal/integration/roomclient"
"hyapp-admin-server/internal/integration/userclient"
"hyapp-admin-server/internal/integration/walletclient"
@ -25,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"
@ -37,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"
@ -197,6 +200,13 @@ func main() {
defer roomConn.Close()
roomClient := roomclient.NewGRPC(roomConn)
robotConn, err := dialBackendGRPC(cfg.RobotService.Addr)
if err != nil {
fatalRuntime("connect_robot_service_failed", err)
}
defer robotConn.Close()
robotClient := robotclient.NewGRPC(robotConn)
activityConn, err := dialBackendGRPC(cfg.ActivityService.Addr)
if err != nil {
fatalRuntime("connect_activity_service_failed", err)
@ -244,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),
@ -279,7 +291,7 @@ func main() {
RegistrationReward: registrationrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
RegionBlock: regionblockmodule.New(userDB, auditHandler),
Resource: resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, cfg.WalletService.RequestTimeout, auditHandler),
RoomAdmin: roomadminmodule.New(userDB, roomClient, auditHandler),
RoomAdmin: roomadminmodule.New(userDB, roomClient, robotClient, auditHandler),
RoomRocket: roomrocketmodule.New(roomClient, auditHandler),
RoomTurnoverReward: roomturnoverrewardmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
Search: searchmodule.New(store),

View File

@ -47,6 +47,9 @@ wallet_service:
room_service:
addr: "10.2.1.16:13001"
request_timeout: "3s"
robot_service:
addr: "10.2.1.16:13011"
request_timeout: "3s"
activity_service:
addr: "10.2.1.16:13006"
request_timeout: "3s"

View File

@ -46,6 +46,9 @@ wallet_service:
room_service:
addr: "127.0.0.1:13001"
request_timeout: "3s"
robot_service:
addr: "127.0.0.1:13011"
request_timeout: "3s"
activity_service:
addr: "127.0.0.1:13006"
request_timeout: "3s"

View File

@ -38,6 +38,7 @@ type Config struct {
Jobs JobsConfig `yaml:"jobs"`
WalletService WalletServiceConfig `yaml:"wallet_service"`
RoomService RoomServiceConfig `yaml:"room_service"`
RobotService RobotServiceConfig `yaml:"robot_service"`
ActivityService ActivityServiceConfig `yaml:"activity_service"`
GameService GameServiceConfig `yaml:"game_service"`
StatisticsService StatisticsServiceConfig `yaml:"statistics_service"`
@ -64,6 +65,11 @@ type RoomServiceConfig struct {
RequestTimeout time.Duration `yaml:"request_timeout"`
}
type RobotServiceConfig struct {
Addr string `yaml:"addr"`
RequestTimeout time.Duration `yaml:"request_timeout"`
}
type ActivityServiceConfig struct {
Addr string `yaml:"addr"`
RequestTimeout time.Duration `yaml:"request_timeout"`
@ -168,6 +174,10 @@ func Default() Config {
Addr: "127.0.0.1:13001",
RequestTimeout: 3 * time.Second,
},
RobotService: RobotServiceConfig{
Addr: "127.0.0.1:13011",
RequestTimeout: 3 * time.Second,
},
ActivityService: ActivityServiceConfig{
Addr: "127.0.0.1:13006",
RequestTimeout: 3 * time.Second,
@ -281,6 +291,13 @@ func (cfg *Config) Normalize() {
if cfg.RoomService.RequestTimeout <= 0 {
cfg.RoomService.RequestTimeout = 3 * time.Second
}
cfg.RobotService.Addr = strings.TrimSpace(cfg.RobotService.Addr)
if cfg.RobotService.Addr == "" {
cfg.RobotService.Addr = "127.0.0.1:13011"
}
if cfg.RobotService.RequestTimeout <= 0 {
cfg.RobotService.RequestTimeout = 3 * time.Second
}
cfg.ActivityService.Addr = strings.TrimSpace(cfg.ActivityService.Addr)
if cfg.ActivityService.RequestTimeout <= 0 {
cfg.ActivityService.RequestTimeout = 3 * time.Second
@ -394,6 +411,12 @@ func (cfg Config) Validate() error {
if cfg.RoomService.RequestTimeout <= 0 {
return errors.New("room_service.request_timeout must be greater than 0")
}
if strings.TrimSpace(cfg.RobotService.Addr) == "" {
return errors.New("robot_service.addr is required")
}
if cfg.RobotService.RequestTimeout <= 0 {
return errors.New("robot_service.request_timeout must be greater than 0")
}
if strings.TrimSpace(cfg.ActivityService.Addr) == "" {
return errors.New("activity_service.addr is required")
}

View File

@ -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)
}

View File

@ -0,0 +1,83 @@
package robotclient
import (
"context"
"fmt"
"strings"
"time"
"hyapp-admin-server/internal/appctx"
robotv1 "hyapp.local/api/proto/robot/v1"
"google.golang.org/grpc"
)
type Client interface {
ListRoomRobots(ctx context.Context, req ListRoomRobotsRequest) (ListRoomRobotsResult, error)
}
type Robot struct {
UserID int64
Status string
RoomScene string
CreatedAtMS int64
UpdatedAtMS int64
LastUsedAtMS int64
UsedCount int64
}
type ListRoomRobotsRequest struct {
RoomScene string
Status string
PageSize int32
Cursor string
}
type ListRoomRobotsResult struct {
Robots []Robot
NextCursor string
ServerTimeMS int64
}
type GRPCClient struct {
client robotv1.RoomRobotServiceClient
}
func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
return &GRPCClient{client: robotv1.NewRoomRobotServiceClient(conn)}
}
func (c *GRPCClient) ListRoomRobots(ctx context.Context, req ListRoomRobotsRequest) (ListRoomRobotsResult, error) {
if c == nil || c.client == nil {
return ListRoomRobotsResult{}, fmt.Errorf("robot service client is not configured")
}
resp, err := c.client.ListRoomRobots(ctx, &robotv1.ListRoomRobotsRequest{
Meta: &robotv1.RequestMeta{
RequestId: fmt.Sprintf("admin-room-robots-%d", time.Now().UnixMilli()),
Caller: "admin-server",
AppCode: appctx.FromContext(ctx),
SentAtMs: time.Now().UnixMilli(),
ActorUserId: 0,
},
RoomScene: strings.TrimSpace(req.RoomScene),
Status: strings.TrimSpace(req.Status),
PageSize: req.PageSize,
Cursor: strings.TrimSpace(req.Cursor),
})
if err != nil {
return ListRoomRobotsResult{}, err
}
items := make([]Robot, 0, len(resp.GetRobots()))
for _, item := range resp.GetRobots() {
items = append(items, Robot{
UserID: item.GetUserId(),
Status: item.GetStatus(),
RoomScene: item.GetRoomScene(),
CreatedAtMS: item.GetCreatedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
LastUsedAtMS: item.GetLastUsedAtMs(),
UsedCount: item.GetUsedCount(),
})
}
return ListRoomRobotsResult{Robots: items, NextCursor: resp.GetNextCursor(), ServerTimeMS: resp.GetServerTimeMs()}, nil
}

View File

@ -24,6 +24,10 @@ type Client interface {
ListRoomPins(ctx context.Context, req ListRoomPinsRequest) (ListRoomPinsResult, error)
CreateRoomPin(ctx context.Context, req CreateRoomPinRequest) (RoomPin, error)
CancelRoomPin(ctx context.Context, req CancelRoomPinRequest) (RoomPin, error)
ListRobotRooms(ctx context.Context, req ListRobotRoomsRequest) (ListRobotRoomsResult, error)
CreateRobotRoom(ctx context.Context, req CreateRobotRoomRequest) (RobotRoom, error)
SetRobotRoomStatus(ctx context.Context, req SetRobotRoomStatusRequest) (RobotRoom, error)
FilterAvailableRoomRobots(ctx context.Context, userIDs []int64) (FilterAvailableRoomRobotsResult, error)
CloseRoom(ctx context.Context, req CloseRoomRequest) (*CloseRoomResult, error)
ReopenRoom(ctx context.Context, req ReopenRoomRequest) (*CloseRoomResult, error)
}
@ -214,6 +218,66 @@ type CancelRoomPinRequest struct {
AdminID uint64
}
type RobotRoomGiftRule struct {
GiftIDs []string
LuckyGiftIDs []string
NormalGiftIntervalMS int64
LuckyComboMin int64
LuckyComboMax int64
LuckyPauseMinMS int64
LuckyPauseMaxMS int64
}
type RobotRoom struct {
AppCode string
RoomID string
RoomShortID string
Title string
CoverURL string
VisibleRegionID int64
Status string
OwnerRobotUserID int64
RobotUserIDs []int64
GiftRule RobotRoomGiftRule
CreatedByAdminID uint64
CreatedAtMS int64
UpdatedAtMS int64
}
type ListRobotRoomsRequest struct {
Page int
PageSize int
Status string
}
type ListRobotRoomsResult struct {
Rooms []RobotRoom
Total int64
}
type CreateRobotRoomRequest struct {
OwnerRobotUserID int64
CandidateRobotUserIDs []int64
MinRobotCount int32
MaxRobotCount int32
RoomName string
RoomAvatar string
VisibleRegionID int64
GiftRule RobotRoomGiftRule
AdminID uint64
}
type SetRobotRoomStatusRequest struct {
RoomID string
Status string
AdminID uint64
}
type FilterAvailableRoomRobotsResult struct {
AvailableUserIDs []int64
OccupiedUserIDs []int64
}
type CloseRoomRequest struct {
RequestID string
RoomID string
@ -400,6 +464,68 @@ func (c *GRPCClient) CancelRoomPin(ctx context.Context, req CancelRoomPinRequest
return roomPinFromProto(resp.GetPin()), nil
}
func (c *GRPCClient) ListRobotRooms(ctx context.Context, req ListRobotRoomsRequest) (ListRobotRoomsResult, error) {
resp, err := c.queryClient.AdminListRobotRooms(ctx, &roomv1.AdminListRobotRoomsRequest{
Meta: requestMeta(ctx, "", 0, "admin-list-robot-rooms"),
Page: int32(req.Page),
PageSize: int32(req.PageSize),
Status: strings.TrimSpace(req.Status),
})
if err != nil {
return ListRobotRoomsResult{}, err
}
items := make([]RobotRoom, 0, len(resp.GetRooms()))
for _, item := range resp.GetRooms() {
items = append(items, robotRoomFromProto(item))
}
return ListRobotRoomsResult{Rooms: items, Total: resp.GetTotal()}, nil
}
func (c *GRPCClient) CreateRobotRoom(ctx context.Context, req CreateRobotRoomRequest) (RobotRoom, error) {
resp, err := c.client.AdminCreateRobotRoom(ctx, &roomv1.AdminCreateRobotRoomRequest{
Meta: requestMeta(ctx, "", int64(req.AdminID), "admin-create-robot-room"),
OwnerRobotUserId: req.OwnerRobotUserID,
CandidateRobotUserIds: append([]int64(nil), req.CandidateRobotUserIDs...),
MinRobotCount: req.MinRobotCount,
MaxRobotCount: req.MaxRobotCount,
RoomName: strings.TrimSpace(req.RoomName),
RoomAvatar: strings.TrimSpace(req.RoomAvatar),
VisibleRegionId: req.VisibleRegionID,
GiftRule: robotRoomGiftRuleToProto(req.GiftRule),
AdminId: req.AdminID,
})
if err != nil {
return RobotRoom{}, err
}
return robotRoomFromProto(resp.GetRoom()), nil
}
func (c *GRPCClient) SetRobotRoomStatus(ctx context.Context, req SetRobotRoomStatusRequest) (RobotRoom, error) {
resp, err := c.client.AdminSetRobotRoomStatus(ctx, &roomv1.AdminSetRobotRoomStatusRequest{
Meta: requestMeta(ctx, strings.TrimSpace(req.RoomID), int64(req.AdminID), "admin-set-robot-room-status"),
Status: strings.TrimSpace(req.Status),
AdminId: req.AdminID,
})
if err != nil {
return RobotRoom{}, err
}
return robotRoomFromProto(resp.GetRoom()), nil
}
func (c *GRPCClient) FilterAvailableRoomRobots(ctx context.Context, userIDs []int64) (FilterAvailableRoomRobotsResult, error) {
resp, err := c.queryClient.AdminFilterAvailableRoomRobots(ctx, &roomv1.AdminFilterAvailableRoomRobotsRequest{
Meta: requestMeta(ctx, "", 0, "admin-filter-available-room-robots"),
UserIds: append([]int64(nil), userIDs...),
})
if err != nil {
return FilterAvailableRoomRobotsResult{}, err
}
return FilterAvailableRoomRobotsResult{
AvailableUserIDs: append([]int64(nil), resp.GetAvailableUserIds()...),
OccupiedUserIDs: append([]int64(nil), resp.GetOccupiedUserIds()...),
}, nil
}
func (c *GRPCClient) CloseRoom(ctx context.Context, req CloseRoomRequest) (*CloseRoomResult, error) {
return c.setRoomLifecycle(ctx, req.RequestID, req.RoomID, req.ActorUserID, firstNonEmpty(req.Reason, "admin_closed"))
}
@ -560,6 +686,54 @@ func roomPinFromProto(input *roomv1.AdminRoomPin) RoomPin {
}
}
func robotRoomFromProto(input *roomv1.AdminRobotRoom) RobotRoom {
if input == nil {
return RobotRoom{}
}
return RobotRoom{
AppCode: input.GetAppCode(),
RoomID: input.GetRoomId(),
RoomShortID: input.GetRoomShortId(),
Title: input.GetTitle(),
CoverURL: input.GetCoverUrl(),
VisibleRegionID: input.GetVisibleRegionId(),
Status: input.GetStatus(),
OwnerRobotUserID: input.GetOwnerRobotUserId(),
RobotUserIDs: append([]int64(nil), input.GetRobotUserIds()...),
GiftRule: robotRoomGiftRuleFromProto(input.GetGiftRule()),
CreatedByAdminID: input.GetCreatedByAdminId(),
CreatedAtMS: input.GetCreatedAtMs(),
UpdatedAtMS: input.GetUpdatedAtMs(),
}
}
func robotRoomGiftRuleFromProto(input *roomv1.AdminRobotRoomGiftRule) RobotRoomGiftRule {
if input == nil {
return RobotRoomGiftRule{}
}
return RobotRoomGiftRule{
GiftIDs: append([]string(nil), input.GetGiftIds()...),
LuckyGiftIDs: append([]string(nil), input.GetLuckyGiftIds()...),
NormalGiftIntervalMS: input.GetNormalGiftIntervalMs(),
LuckyComboMin: input.GetLuckyComboMin(),
LuckyComboMax: input.GetLuckyComboMax(),
LuckyPauseMinMS: input.GetLuckyPauseMinMs(),
LuckyPauseMaxMS: input.GetLuckyPauseMaxMs(),
}
}
func robotRoomGiftRuleToProto(input RobotRoomGiftRule) *roomv1.AdminRobotRoomGiftRule {
return &roomv1.AdminRobotRoomGiftRule{
GiftIds: append([]string(nil), input.GiftIDs...),
LuckyGiftIds: append([]string(nil), input.LuckyGiftIDs...),
NormalGiftIntervalMs: input.NormalGiftIntervalMS,
LuckyComboMin: input.LuckyComboMin,
LuckyComboMax: input.LuckyComboMax,
LuckyPauseMinMs: input.LuckyPauseMinMS,
LuckyPauseMaxMs: input.LuckyPauseMaxMS,
}
}
func roomRocketConfigToProto(input RoomRocketConfig) *roomv1.AdminRoomRocketConfig {
return &roomv1.AdminRoomRocketConfig{
AppCode: input.AppCode,

View 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
}

View 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)
}

View File

@ -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) {

View File

@ -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

View 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 ""
}

View File

@ -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")
}
}

View 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)
}

View File

@ -63,6 +63,35 @@ func (h *Handler) ListAgencies(c *gin.Context) {
response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
}
func (h *Handler) ListManagers(c *gin.Context) {
query, ok := parseListQuery(c)
if !ok {
return
}
items, total, err := h.service.ListManagers(c.Request.Context(), query)
if err != nil {
response.ServerError(c, "获取经理列表失败")
return
}
response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
}
func (h *Handler) CreateManager(c *gin.Context) {
var req createManagerRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "经理参数不正确")
return
}
item, err := h.service.CreateManager(c.Request.Context(), adminActorID(c), middleware.CurrentRequestID(c), req)
if err != nil {
response.BadRequest(c, err.Error())
return
}
writeHostOrgAuditLog(c, h.audit, "create-manager", "manager_profiles", item.UserID,
fmt.Sprintf("user_id=%d contact=%q", item.UserID, strings.TrimSpace(req.Contact)))
response.Created(c, item)
}
func (h *Handler) ListHosts(c *gin.Context) {
query, ok := parseListQuery(c)
if !ok {

View File

@ -5,6 +5,7 @@ import (
"database/sql"
"fmt"
"sort"
"strconv"
"strings"
"time"
@ -41,6 +42,21 @@ type CoinSellerListItem struct {
UpdatedAtMs int64 `json:"updatedAtMs"`
}
type ManagerListItem struct {
UserID int64 `json:"userId,string"`
DisplayUserID string `json:"displayUserId"`
Username string `json:"username"`
Avatar string `json:"avatar"`
RegionID int64 `json:"regionId"`
RegionName string `json:"regionName"`
Status string `json:"status"`
Contact string `json:"contact"`
BDLeaderCount int64 `json:"bdLeaderCount"`
LastInvitedAtMs int64 `json:"lastInvitedAtMs"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
}
type CoinSellerSalaryRateTier struct {
RegionID int64 `json:"regionId,string"`
MinUSDMinor int64 `json:"minUsdMinor"`
@ -557,6 +573,118 @@ func (r *Reader) DeleteBDLeader(ctx context.Context, userID int64) (*userclient.
return item, nil
}
// CreateManagerProfile 创建或恢复经理身份。
// Manager 是 manager_profiles 表里的独立身份;这里只写经理事实和联系方式,不改 Host/Agency/BD 任一关系。
func (r *Reader) CreateManagerProfile(ctx context.Context, userID int64, actorID int64, contact string) (*ManagerListItem, error) {
if r == nil || r.db == nil {
return nil, errUserDBNotConfigured()
}
if userID <= 0 {
return nil, fmt.Errorf("target_user_id is required")
}
contact = strings.TrimSpace(contact)
if len([]rune(contact)) > 128 {
return nil, fmt.Errorf("contact is too long")
}
nowMs := time.Now().UnixMilli()
appCode := appctx.FromContext(ctx)
// ON DUPLICATE KEY 用于后台重复添加同一经理时刷新联系方式并恢复启用状态,
// 不新增第二条身份,也不触碰该经理已经邀请出的 BD Leader/Admin 归属统计。
if _, err := r.db.ExecContext(ctx, `
INSERT INTO manager_profiles (
app_code, user_id, status, contact_info, created_by_admin_id, created_at_ms, updated_at_ms
) VALUES (?, ?, 'active', ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
status = 'active',
contact_info = VALUES(contact_info),
updated_at_ms = VALUES(updated_at_ms)
`, appCode, userID, contact, actorID, nowMs, nowMs); err != nil {
return nil, err
}
items, _, err := r.ListManagers(ctx, listQuery{Keyword: strconv.FormatInt(userID, 10), Page: 1, PageSize: 1})
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, fmt.Errorf("manager not found")
}
return items[0], nil
}
// ListManagers 按 manager_profiles 的独立身份事实读取经理列表。
// 经理是 BD Leader/Admin 的上级,不属于 Host/Agency/BD 链路;下级 BD Leader 只作为统计信息左连汇总。
func (r *Reader) ListManagers(ctx context.Context, query listQuery) ([]*ManagerListItem, int64, error) {
if r == nil || r.db == nil {
return nil, 0, errUserDBNotConfigured()
}
appCode := appctx.FromContext(ctx)
whereSQL := `
FROM manager_profiles mp
JOIN users manager ON manager.app_code = mp.app_code AND manager.user_id = mp.user_id
LEFT JOIN bd_leader_profiles bl ON bl.app_code = mp.app_code AND bl.created_by_user_id = mp.user_id
` + userCountryRegionJoin("manager", "manager_country_region", "manager_region") + `
WHERE mp.app_code = ?`
args := []any{appCode}
if query.Status != "" {
whereSQL += " AND mp.status = ?"
args = append(args, query.Status)
}
if query.RegionID > 0 {
whereSQL += " AND manager_region.region_id = ?"
args = append(args, query.RegionID)
}
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
like := "%" + keyword + "%"
managerSQL, managerArgs := shared.UserIdentityKeywordSQL("manager", "mp.user_id", keyword, time.Now().UnixMilli())
whereSQL += " AND (" + managerSQL + " OR manager_region.name LIKE ?)"
args = append(args, managerArgs...)
args = append(args, like)
}
var total int64
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT mp.user_id) `+whereSQL, args...).Scan(&total); err != nil {
return nil, 0, err
}
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
SELECT mp.user_id, COALESCE(manager.current_display_user_id, ''), COALESCE(manager.username, ''),
COALESCE(manager.avatar, ''), `+userCountryRegionIDSQL("manager_region")+`, `+userCountryRegionNameSQL("manager_region")+`,
mp.status, COALESCE(mp.contact_info, ''), COUNT(DISTINCT bl.user_id), COALESCE(MAX(bl.created_at_ms), 0),
mp.created_at_ms, mp.updated_at_ms
%s
GROUP BY mp.user_id, manager.current_display_user_id, manager.username, manager.avatar,
manager_region.region_id, manager_region.name, mp.status, mp.contact_info, mp.created_at_ms, mp.updated_at_ms
ORDER BY mp.updated_at_ms DESC, mp.user_id DESC
LIMIT ? OFFSET ?
`, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
items := make([]*ManagerListItem, 0, query.PageSize)
for rows.Next() {
item := &ManagerListItem{}
if err := rows.Scan(
&item.UserID,
&item.DisplayUserID,
&item.Username,
&item.Avatar,
&item.RegionID,
&item.RegionName,
&item.Status,
&item.Contact,
&item.BDLeaderCount,
&item.LastInvitedAtMs,
&item.CreatedAtMs,
&item.UpdatedAtMs,
); err != nil {
return nil, 0, err
}
items = append(items, item)
}
return items, total, rows.Err()
}
// ListAgencies 只读 user-service 的 Agency 事实表;后台关闭和入会开关仍走写命令。
func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclient.Agency, int64, error) {
if r == nil || r.db == nil {

View File

@ -60,6 +60,11 @@ type createBDLeaderRequest struct {
Reason string `json:"reason"`
}
type createManagerRequest struct {
Contact string `json:"contact"`
TargetUserID flexibleInt64 `json:"targetUserId" binding:"required"`
}
type createBDRequest struct {
CommandID string `json:"commandId" binding:"required"`
TargetUserID flexibleInt64 `json:"targetUserId" binding:"required"`

View File

@ -15,6 +15,8 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.GET("/admin/bds", middleware.RequirePermission("bd:view"), h.ListBDs)
protected.POST("/admin/bds", middleware.RequirePermission("bd:create"), h.CreateBD)
protected.PATCH("/admin/bds/:user_id/status", middleware.RequirePermission("bd:update"), h.SetBDStatus)
protected.GET("/admin/managers", middleware.RequirePermission("bd:view"), h.ListManagers)
protected.POST("/admin/managers", middleware.RequirePermission("bd:create"), h.CreateManager)
protected.GET("/admin/agencies", middleware.RequirePermission("agency:view"), h.ListAgencies)
protected.POST("/admin/agencies", middleware.RequirePermission("agency:create"), h.CreateAgency)
protected.POST("/admin/agencies/:agency_id/close", middleware.RequirePermission("agency:status"), h.CloseAgency)

View File

@ -50,6 +50,20 @@ func (s *Service) ListAgencies(ctx context.Context, query listQuery) ([]*usercli
return s.reader.ListAgencies(ctx, query)
}
func (s *Service) ListManagers(ctx context.Context, query listQuery) ([]*ManagerListItem, int64, error) {
query = normalizeListQuery(query)
return s.reader.ListManagers(ctx, query)
}
func (s *Service) CreateManager(ctx context.Context, actorID int64, requestID string, req createManagerRequest) (*ManagerListItem, error) {
targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID.Int64())
if err != nil {
return nil, err
}
// 经理身份只落 manager_profiles 表;联系方式作为后台运营资料保存,不参与经理中心授权。
return s.reader.CreateManagerProfile(ctx, targetUserID, actorID, req.Contact)
}
func (s *Service) ListHosts(ctx context.Context, query listQuery) ([]*userclient.HostProfile, int64, error) {
query = normalizeListQuery(query)
return s.reader.ListHostProfiles(ctx, query)

View File

@ -1,6 +1,7 @@
package hostorg
import (
"encoding/json"
"strings"
"testing"
@ -17,6 +18,38 @@ func TestNormalizeListQueryAllowsComputedSorts(t *testing.T) {
}
}
// TestManagerListItemJSONHasNoAgencyOrParentBD 锁定后台经理列表的响应口径:经理是 BD Leader/Admin 的上级,不暴露 Agency 或上级 BD 字段。
func TestManagerListItemJSONHasNoAgencyOrParentBD(t *testing.T) {
item := ManagerListItem{
UserID: 316033326332776448,
DisplayUserID: "165549",
Username: "Kitty",
Status: "active",
Contact: "+63",
BDLeaderCount: 3,
LastInvitedAtMs: 1720000000000,
}
payload, err := json.Marshal(item)
if err != nil {
t.Fatalf("marshal manager list item: %v", err)
}
body := string(payload)
for _, want := range []string{
`"userId":"316033326332776448"`,
`"displayUserId":"165549"`,
`"contact":"+63"`,
`"bdLeaderCount":3`,
`"lastInvitedAtMs":1720000000000`,
} {
if !strings.Contains(body, want) {
t.Fatalf("manager json should contain %s, got %s", want, body)
}
}
if strings.Contains(body, "parentBd") || strings.Contains(body, "ParentBD") || strings.Contains(body, "agency") || strings.Contains(body, "Agency") {
t.Fatalf("manager json must not expose agency or parent bd fields: %s", body)
}
}
// TestBDRoleFromStatusPathUsesLeaderTableWithAPIPrefix 锁定线上真实 FullPath 带 /api/v1 前缀时BD Leader 状态仍然写独立负责人表。
func TestBDRoleFromStatusPathUsesLeaderTableWithAPIPrefix(t *testing.T) {
role, target := bdRoleFromStatusPath("/api/v1/admin/bd-leaders/:user_id/status")

View File

@ -0,0 +1,274 @@
package payment
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"sort"
"strconv"
"strings"
"time"
walletv1 "hyapp.local/api/proto/wallet/v1"
)
type exchangeRateClient interface {
LatestUSDRates(ctx context.Context, currencies []string) (exchangeRateResult, error)
}
type exchangeRateResult struct {
Rates map[string]string `json:"rates"`
SourceNames []string `json:"sourceNames"`
Sources []exchangeRateSourceStatusDTO `json:"sources"`
}
type exchangeRateSourceStatusDTO struct {
Name string `json:"name"`
URL string `json:"url"`
Success bool `json:"success"`
Currencies []string `json:"currencies,omitempty"`
Error string `json:"error,omitempty"`
}
type thirdPartyPaymentRateSyncDTO struct {
UpdatedCount int `json:"updatedCount"`
SkippedCount int `json:"skippedCount"`
MissingCurrencies []string `json:"missingCurrencies"`
SourceNames []string `json:"sourceNames"`
Sources []exchangeRateSourceStatusDTO `json:"sources"`
Rates map[string]string `json:"rates"`
}
type httpExchangeRateClient struct {
client *http.Client
sources []exchangeRateSource
}
type exchangeRateSource struct {
name string
url string
parse func([]byte) (map[string]float64, error)
}
func newDefaultExchangeRateClient() exchangeRateClient {
return &httpExchangeRateClient{
client: &http.Client{Timeout: 8 * time.Second},
sources: []exchangeRateSource{
{name: "open.er-api.com", url: "https://open.er-api.com/v6/latest/USD", parse: parseOpenExchangeRateAPI},
{name: "fawazahmed0-currency-api", url: "https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json", parse: parseFawazCurrencyAPI},
{name: "FloatRates", url: "https://www.floatrates.com/daily/usd.json", parse: parseFloatRates},
{name: "Frankfurter", url: "https://api.frankfurter.dev/v1/latest?base=USD", parse: parseFrankfurterRates},
},
}
}
func (c *httpExchangeRateClient) LatestUSDRates(ctx context.Context, currencies []string) (exchangeRateResult, error) {
required, rates := splitRequiredCurrencies(currencies)
if len(required) == 0 {
return exchangeRateResult{Rates: rates}, nil
}
sourceNames := make([]string, 0, len(c.sources))
sourceStatuses := make([]exchangeRateSourceStatusDTO, 0, len(c.sources))
for _, source := range c.sources {
// 每个源独立失败,不阻断后续源;同一个币种只取最先成功源,避免不同源的更新时间混在同一个币种上来回抖动。
sourceRates, err := c.fetchSource(ctx, source)
if err != nil {
sourceStatuses = append(sourceStatuses, exchangeRateSourceStatusDTO{Name: source.name, URL: source.url, Success: false, Error: err.Error()})
continue
}
matched := make([]string, 0, len(required))
for currency := range required {
if _, exists := rates[currency]; exists {
continue
}
value := sourceRates[currency]
if value <= 0 {
continue
}
rates[currency] = formatExchangeRate(value)
matched = append(matched, currency)
}
sort.Strings(matched)
if len(matched) > 0 {
sourceNames = append(sourceNames, source.name)
}
sourceStatuses = append(sourceStatuses, exchangeRateSourceStatusDTO{Name: source.name, URL: source.url, Success: true, Currencies: matched})
if len(rates)-usdRateCount(rates) == len(required) {
break
}
}
if len(rates)-usdRateCount(rates) == 0 {
return exchangeRateResult{}, errors.New("no exchange rate source returned requested currencies")
}
return exchangeRateResult{Rates: rates, SourceNames: sourceNames, Sources: sourceStatuses}, nil
}
func (c *httpExchangeRateClient) fetchSource(ctx context.Context, source exchangeRateSource) (map[string]float64, error) {
client := c.client
if client == nil {
client = &http.Client{Timeout: 8 * time.Second}
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, source.url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "HYAppAdmin/1.0 exchange-rate-sync")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("http %d", resp.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
if err != nil {
return nil, err
}
return source.parse(body)
}
func parseOpenExchangeRateAPI(body []byte) (map[string]float64, error) {
var payload struct {
Result string `json:"result"`
Rates map[string]float64 `json:"rates"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
if payload.Result != "" && payload.Result != "success" {
return nil, fmt.Errorf("result %s", payload.Result)
}
return normalizeFloatRateMap(payload.Rates), nil
}
func parseFrankfurterRates(body []byte) (map[string]float64, error) {
var payload struct {
Rates map[string]float64 `json:"rates"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
return normalizeFloatRateMap(payload.Rates), nil
}
func parseFawazCurrencyAPI(body []byte) (map[string]float64, error) {
var payload struct {
Rates map[string]float64 `json:"usd"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
return normalizeFloatRateMap(payload.Rates), nil
}
func parseFloatRates(body []byte) (map[string]float64, error) {
var payload map[string]struct {
Code string `json:"code"`
AlphaCode string `json:"alphaCode"`
Rate string `json:"rate"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
rates := make(map[string]float64, len(payload))
for key, item := range payload {
currency := strings.ToUpper(firstNonEmptyString(item.AlphaCode, item.Code, key))
value, err := strconv.ParseFloat(strings.TrimSpace(item.Rate), 64)
if currency == "" || err != nil || value <= 0 {
continue
}
rates[currency] = value
}
return rates, nil
}
func normalizeFloatRateMap(input map[string]float64) map[string]float64 {
rates := make(map[string]float64, len(input))
for currency, value := range input {
currency = strings.ToUpper(strings.TrimSpace(currency))
if currency == "" || value <= 0 {
continue
}
rates[currency] = value
}
return rates
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return ""
}
func collectThirdPartyPaymentMethods(channels []*walletv1.ThirdPartyPaymentChannel) []*walletv1.ThirdPartyPaymentMethod {
seen := make(map[int64]struct{})
methods := make([]*walletv1.ThirdPartyPaymentMethod, 0)
for _, channel := range channels {
for _, method := range channel.GetMethods() {
if method.GetMethodId() <= 0 {
continue
}
if _, exists := seen[method.GetMethodId()]; exists {
continue
}
seen[method.GetMethodId()] = struct{}{}
methods = append(methods, method)
}
}
return methods
}
func collectThirdPartyPaymentCurrencies(methods []*walletv1.ThirdPartyPaymentMethod) []string {
currencies := make(map[string]struct{})
for _, method := range methods {
if currency := strings.ToUpper(strings.TrimSpace(method.GetCurrencyCode())); currency != "" {
currencies[currency] = struct{}{}
}
}
return sortedMapKeys(currencies)
}
func splitRequiredCurrencies(currencies []string) (map[string]struct{}, map[string]string) {
required := make(map[string]struct{})
rates := make(map[string]string)
for _, currency := range currencies {
currency = strings.ToUpper(strings.TrimSpace(currency))
if currency == "" {
continue
}
if currency == "USD" {
rates[currency] = "1.00000000"
continue
}
required[currency] = struct{}{}
}
return required, rates
}
func usdRateCount(rates map[string]string) int {
if rates["USD"] == "" {
return 0
}
return 1
}
func sortedMapKeys(values map[string]struct{}) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func formatExchangeRate(value float64) string {
return strconv.FormatFloat(value, 'f', 8, 64)
}

View File

@ -23,14 +23,15 @@ import (
const usdtMicroUnit int64 = 1_000_000
type Handler struct {
wallet walletclient.Client
userDB *sql.DB
audit shared.OperationLogger
wallet walletclient.Client
userDB *sql.DB
audit shared.OperationLogger
exchangeRates exchangeRateClient
}
// New 组装支付后台 handlerwallet 负责账单和商品事实userDB 只补后台列表需要的用户展示资料。
func New(wallet walletclient.Client, userDB *sql.DB, audit shared.OperationLogger) *Handler {
return &Handler{wallet: wallet, userDB: userDB, audit: audit}
return &Handler{wallet: wallet, userDB: userDB, audit: audit, exchangeRates: newDefaultExchangeRateClient()}
}
func (h *Handler) ListRechargeBills(c *gin.Context) {
@ -314,6 +315,66 @@ func (h *Handler) UpdateThirdPartyPaymentRate(c *gin.Context) {
response.OK(c, item)
}
func (h *Handler) SyncThirdPartyPaymentRates(c *gin.Context) {
resp, err := h.wallet.ListThirdPartyPaymentChannels(c.Request.Context(), &walletv1.ListThirdPartyPaymentChannelsRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
ProviderCode: strings.TrimSpace(firstQuery(c, "provider_code", "providerCode")),
IncludeDisabledMethods: true,
})
if err != nil {
writeWalletError(c, err, "获取三方支付方式失败")
return
}
methods := collectThirdPartyPaymentMethods(resp.GetChannels())
currencies := collectThirdPartyPaymentCurrencies(methods)
if len(currencies) == 0 {
response.OK(c, thirdPartyPaymentRateSyncDTO{UpdatedCount: 0})
return
}
// 汇率同步必须以服务端实时拉取结果为准:前端按钮只触发动作,不传任何汇率,避免浏览器篡改本币金额。
rateClient := h.exchangeRates
if rateClient == nil {
rateClient = newDefaultExchangeRateClient()
}
rateResult, err := rateClient.LatestUSDRates(c.Request.Context(), currencies)
if err != nil {
response.ServerError(c, "同步支付汇率失败")
return
}
updated := 0
skipped := make(map[string]struct{})
for _, method := range methods {
currencyCode := strings.ToUpper(strings.TrimSpace(method.GetCurrencyCode()))
rate := rateResult.Rates[currencyCode]
if rate == "" {
skipped[currencyCode] = struct{}{}
continue
}
if _, err := h.wallet.UpdateThirdPartyPaymentRate(c.Request.Context(), &walletv1.UpdateThirdPartyPaymentRateRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
MethodId: method.GetMethodId(),
UsdToCurrencyRate: rate,
OperatorUserId: actorID(c),
}); err != nil {
writeWalletError(c, err, "写入支付汇率失败")
return
}
updated++
}
skippedCurrencies := sortedMapKeys(skipped)
shared.OperationLog(c, h.audit, "sync-third-party-payment-rates", "third_party_payment_methods", "success", strings.Join(rateResult.SourceNames, ","))
response.OK(c, thirdPartyPaymentRateSyncDTO{
UpdatedCount: updated,
SkippedCount: len(skippedCurrencies),
MissingCurrencies: skippedCurrencies,
SourceNames: rateResult.SourceNames,
Sources: rateResult.Sources,
Rates: rateResult.Rates,
})
}
type rechargeProductRequest struct {
AmountUSDT string `json:"amountUsdt"`
AmountUSDTMicro int64 `json:"amountUsdtMicro"`

View File

@ -108,6 +108,58 @@ func TestUpdateThirdPartyPaymentRateTrimsRateAndForwardsOperator(t *testing.T) {
}
}
func TestSyncThirdPartyPaymentRatesUpdatesAllMethodsFromFetchedCurrencyRates(t *testing.T) {
wallet := &mockPaymentWallet{thirdPartyChannelsResp: &walletv1.ListThirdPartyPaymentChannelsResponse{
Channels: []*walletv1.ThirdPartyPaymentChannel{{
AppCode: "lalu",
ProviderCode: "mifapay",
ProviderName: "MiFaPay",
Status: "active",
Methods: []*walletv1.ThirdPartyPaymentMethod{
{MethodId: 810, CurrencyCode: "SAR", Status: "active"},
{MethodId: 811, CurrencyCode: "BHD", Status: "disabled"},
},
}},
}}
handler := New(wallet, nil, nil)
handler.exchangeRates = fakeExchangeRateClient{result: exchangeRateResult{
Rates: map[string]string{"SAR": "3.75000000", "BHD": "0.37600000"},
SourceNames: []string{"open.er-api.com"},
}}
router := newPaymentHandlerTestRouter(handler)
request := httptest.NewRequest(http.MethodPost, "/admin/payment/third-party-rates/sync", nil)
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 wallet.lastThirdPartyChannels == nil || !wallet.lastThirdPartyChannels.GetIncludeDisabledMethods() {
t.Fatalf("sync must read active and disabled methods: %+v", wallet.lastThirdPartyChannels)
}
if len(wallet.updateRateRequests) != 2 {
t.Fatalf("sync should update every method with a fetched currency rate: %+v", wallet.updateRateRequests)
}
got := map[int64]string{}
for _, req := range wallet.updateRateRequests {
if req.GetOperatorUserId() != 7 {
t.Fatalf("sync operator mismatch: %+v", req)
}
got[req.GetMethodId()] = req.GetUsdToCurrencyRate()
}
if got[810] != "3.75000000" || got[811] != "0.37600000" {
t.Fatalf("synced rates mismatch: %+v", got)
}
var response adminPaymentTestResponse
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode response failed: %v", err)
}
if response.Code != 0 || response.Data["updatedCount"].(float64) != 2 {
t.Fatalf("sync response mismatch: %+v", response)
}
}
func TestCreateRechargeProductForwardsWebCoinSellerAudience(t *testing.T) {
wallet := &mockPaymentWallet{}
router := newPaymentHandlerTestRouter(New(wallet, nil, nil))
@ -148,6 +200,7 @@ func newPaymentHandlerTestRouter(handler *Handler) *gin.Engine {
router.GET("/admin/payment/third-party-channels", handler.ListThirdPartyPaymentChannels)
router.PATCH("/admin/payment/third-party-methods/:method_id/status", handler.SetThirdPartyPaymentMethodStatus)
router.PATCH("/admin/payment/third-party-rates/:method_id", handler.UpdateThirdPartyPaymentRate)
router.POST("/admin/payment/third-party-rates/sync", handler.SyncThirdPartyPaymentRates)
router.POST("/admin/payment/recharge-products", handler.CreateRechargeProduct)
return router
}
@ -164,6 +217,7 @@ type mockPaymentWallet struct {
thirdPartyChannelsResp *walletv1.ListThirdPartyPaymentChannelsResponse
lastSetMethodStatus *walletv1.SetThirdPartyPaymentMethodStatusRequest
lastUpdateRate *walletv1.UpdateThirdPartyPaymentRateRequest
updateRateRequests []*walletv1.UpdateThirdPartyPaymentRateRequest
lastCreateProduct *walletv1.CreateRechargeProductRequest
}
@ -195,6 +249,7 @@ func (m *mockPaymentWallet) SetThirdPartyPaymentMethodStatus(_ context.Context,
func (m *mockPaymentWallet) UpdateThirdPartyPaymentRate(_ context.Context, req *walletv1.UpdateThirdPartyPaymentRateRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error) {
m.lastUpdateRate = req
m.updateRateRequests = append(m.updateRateRequests, req)
return &walletv1.ThirdPartyPaymentMethodResponse{Method: &walletv1.ThirdPartyPaymentMethod{
MethodId: req.GetMethodId(),
AppCode: req.GetAppCode(),
@ -208,6 +263,15 @@ func (m *mockPaymentWallet) UpdateThirdPartyPaymentRate(_ context.Context, req *
}}, nil
}
type fakeExchangeRateClient struct {
result exchangeRateResult
err error
}
func (f fakeExchangeRateClient) LatestUSDRates(context.Context, []string) (exchangeRateResult, error) {
return f.result, f.err
}
func (m *mockPaymentWallet) CreateRechargeProduct(_ context.Context, req *walletv1.CreateRechargeProductRequest) (*walletv1.RechargeProductResponse, error) {
m.lastCreateProduct = req
return &walletv1.RechargeProductResponse{Product: &walletv1.RechargeProduct{

View File

@ -15,6 +15,7 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.GET("/admin/payment/third-party-channels", middleware.RequirePermission("payment-third-party:view"), h.ListThirdPartyPaymentChannels)
protected.PATCH("/admin/payment/third-party-methods/:method_id/status", middleware.RequirePermission("payment-third-party:update"), h.SetThirdPartyPaymentMethodStatus)
protected.PATCH("/admin/payment/third-party-rates/:method_id", middleware.RequirePermission("payment-third-party:update"), h.UpdateThirdPartyPaymentRate)
protected.POST("/admin/payment/third-party-rates/sync", middleware.RequirePermission("payment-third-party:update"), h.SyncThirdPartyPaymentRates)
protected.GET("/admin/payment/recharge-products", middleware.RequirePermission("payment-product:view"), h.ListRechargeProducts)
protected.POST("/admin/payment/recharge-products", middleware.RequirePermission("payment-product:create"), h.CreateRechargeProduct)
protected.PUT("/admin/payment/recharge-products/:product_id", middleware.RequirePermission("payment-product:update"), h.UpdateRechargeProduct)

View File

@ -6,6 +6,7 @@ import (
"strconv"
"strings"
"hyapp-admin-server/internal/integration/robotclient"
"hyapp-admin-server/internal/integration/roomclient"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
@ -19,8 +20,8 @@ type Handler struct {
audit shared.OperationLogger
}
func New(userDB *sql.DB, roomClient roomclient.Client, audit shared.OperationLogger) *Handler {
return &Handler{service: NewService(userDB, roomClient), audit: audit}
func New(userDB *sql.DB, roomClient roomclient.Client, robotClient robotclient.Client, audit shared.OperationLogger) *Handler {
return &Handler{service: NewService(userDB, roomClient, robotClient), audit: audit}
}
func (h *Handler) ListRooms(c *gin.Context) {
@ -78,6 +79,48 @@ func (h *Handler) CancelRoomPin(c *gin.Context) {
response.OK(c, pin)
}
func (h *Handler) ListRobotRooms(c *gin.Context) {
query := parseRobotRoomListQuery(c)
items, total, err := h.service.ListRobotRooms(c.Request.Context(), query)
if err != nil {
response.ServerError(c, "获取机器人房间列表失败")
return
}
response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
}
func (h *Handler) ListAvailableRoomRobots(c *gin.Context) {
items, err := h.service.ListAvailableRoomRobots(c.Request.Context())
if err != nil {
response.ServerError(c, "获取可用机器人失败")
return
}
response.OK(c, items)
}
func (h *Handler) CreateRobotRoom(c *gin.Context) {
var req createRobotRoomRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "机器人房间参数不正确")
return
}
room, err := h.service.CreateRobotRoom(c.Request.Context(), req, shared.ActorFromContext(c))
if err != nil {
writeMutationError(c, err, "创建机器人房间失败")
return
}
writeRoomAuditLog(c, h.audit, "create-robot-room", "room_robot_rooms", room.RoomID, "success", "create robot room")
response.Created(c, room)
}
func (h *Handler) StartRobotRoom(c *gin.Context) {
h.setRobotRoomStatus(c, "active", "start-robot-room", "启动机器人房间失败")
}
func (h *Handler) StopRobotRoom(c *gin.Context) {
h.setRobotRoomStatus(c, "stopped", "stop-robot-room", "停止机器人房间失败")
}
func (h *Handler) GetRoomConfig(c *gin.Context) {
config, err := h.service.GetRoomConfig(c.Request.Context())
if err != nil {
@ -134,6 +177,20 @@ func (h *Handler) DeleteRoom(c *gin.Context) {
response.OK(c, gin.H{"deleted": true})
}
func (h *Handler) setRobotRoomStatus(c *gin.Context, status string, action string, fallback string) {
roomID, ok := parseRoomID(c)
if !ok {
return
}
room, err := h.service.SetRobotRoomStatus(c.Request.Context(), roomID, status, shared.ActorFromContext(c))
if err != nil {
writeMutationError(c, err, fallback)
return
}
writeRoomAuditLog(c, h.audit, action, "room_robot_rooms", roomID, "success", action)
response.OK(c, room)
}
func parseListQuery(c *gin.Context) (listQuery, bool) {
options := shared.ListOptions(c)
query := listQuery{
@ -168,6 +225,15 @@ func parseRoomPinListQuery(c *gin.Context) (roomPinListQuery, bool) {
}), true
}
func parseRobotRoomListQuery(c *gin.Context) robotRoomListQuery {
options := shared.ListOptions(c)
return robotRoomListQuery{
Page: options.Page,
PageSize: options.PageSize,
Status: options.Status,
}
}
func firstQueryValue(c *gin.Context, names ...string) string {
for _, name := range names {
if value := strings.TrimSpace(c.Query(name)); value != "" {

View File

@ -19,6 +19,29 @@ type roomPinListQuery struct {
Status string
}
type robotRoomListQuery struct {
Page int
PageSize int
Status string
}
type createRobotRoomRequest struct {
OwnerRobotUserID int64 `json:"ownerRobotUserId"`
CandidateRobotUserIDs []int64 `json:"candidateRobotUserIds"`
MinRobotCount int32 `json:"minRobotCount"`
MaxRobotCount int32 `json:"maxRobotCount"`
RoomName string `json:"roomName"`
RoomAvatar string `json:"roomAvatar"`
VisibleRegionID int64 `json:"visibleRegionId"`
GiftIDs []string `json:"giftIds"`
LuckyGiftIDs []string `json:"luckyGiftIds"`
NormalGiftIntervalMS int64 `json:"normalGiftIntervalMs"`
LuckyComboMin int64 `json:"luckyComboMin"`
LuckyComboMax int64 `json:"luckyComboMax"`
LuckyPauseMinMS int64 `json:"luckyPauseMinMs"`
LuckyPauseMaxMS int64 `json:"luckyPauseMaxMs"`
}
type createRoomPinRequest struct {
DurationDays int64 `json:"durationDays"`
ExpiresAtMS int64 `json:"expiresAtMs"`

View File

@ -0,0 +1,300 @@
package roomadmin
import (
"context"
"fmt"
"strconv"
"strings"
"hyapp-admin-server/internal/integration/robotclient"
"hyapp-admin-server/internal/integration/roomclient"
"hyapp-admin-server/internal/modules/shared"
)
const defaultRoomRobotScene = "voice"
type RobotRoom struct {
AppCode string `json:"appCode"`
RoomID string `json:"roomId"`
RoomShortID string `json:"roomShortId"`
Title string `json:"title"`
CoverURL string `json:"coverUrl"`
VisibleRegionID int64 `json:"visibleRegionId"`
RegionName string `json:"regionName"`
Status string `json:"status"`
OwnerRobotUserID string `json:"ownerRobotUserId"`
Owner RoomOwner `json:"owner"`
RobotUserIDs []string `json:"robotUserIds"`
Robots []RoomOwner `json:"robots"`
GiftRule RobotGiftRule `json:"giftRule"`
CreatedByAdminID uint64 `json:"createdByAdminId"`
CreatedAtMS int64 `json:"createdAtMs"`
UpdatedAtMS int64 `json:"updatedAtMs"`
}
type RobotGiftRule struct {
GiftIDs []string `json:"giftIds"`
LuckyGiftIDs []string `json:"luckyGiftIds"`
NormalGiftIntervalMS int64 `json:"normalGiftIntervalMs"`
LuckyComboMin int64 `json:"luckyComboMin"`
LuckyComboMax int64 `json:"luckyComboMax"`
LuckyPauseMinMS int64 `json:"luckyPauseMinMs"`
LuckyPauseMaxMS int64 `json:"luckyPauseMaxMs"`
}
type AvailableRoomRobot struct {
UserID string `json:"userId"`
Status string `json:"status"`
RoomScene string `json:"roomScene"`
LastUsedAtMS int64 `json:"lastUsedAtMs"`
UsedCount int64 `json:"usedCount"`
User RoomOwner `json:"user"`
}
func (s *Service) ListRobotRooms(ctx context.Context, query robotRoomListQuery) ([]RobotRoom, int64, error) {
if s.roomClient == nil {
return nil, 0, fmt.Errorf("room service client is not configured")
}
if query.Page <= 0 {
query.Page = 1
}
if query.PageSize <= 0 || query.PageSize > 100 {
query.PageSize = 50
}
result, err := s.roomClient.ListRobotRooms(ctx, roomclient.ListRobotRoomsRequest{
Page: query.Page,
PageSize: query.PageSize,
Status: normalizeRobotRoomListStatus(query.Status),
})
if err != nil {
return nil, 0, err
}
items := robotRoomsFromClient(result.Rooms)
if err := s.fillRobotRoomDetails(ctx, items); err != nil {
return nil, 0, err
}
return items, result.Total, nil
}
func normalizeRobotRoomListStatus(status string) string {
switch strings.TrimSpace(status) {
case "active", "stopped":
return strings.TrimSpace(status)
default:
return ""
}
}
func (s *Service) ListAvailableRoomRobots(ctx context.Context) ([]AvailableRoomRobot, error) {
if s.robotClient == nil || s.roomClient == nil {
return nil, fmt.Errorf("robot or room service client is not configured")
}
result, err := s.robotClient.ListRoomRobots(ctx, robotclient.ListRoomRobotsRequest{
RoomScene: defaultRoomRobotScene,
Status: "active",
PageSize: 200,
})
if err != nil {
return nil, err
}
userIDs := make([]int64, 0, len(result.Robots))
for _, item := range result.Robots {
if item.UserID > 0 {
userIDs = append(userIDs, item.UserID)
}
}
available, err := s.roomClient.FilterAvailableRoomRobots(ctx, userIDs)
if err != nil {
return nil, err
}
allowed := make(map[int64]bool, len(available.AvailableUserIDs))
for _, userID := range available.AvailableUserIDs {
allowed[userID] = true
}
owners, err := s.queryRoomOwners(ctx, available.AvailableUserIDs)
if err != nil {
return nil, err
}
items := make([]AvailableRoomRobot, 0, len(available.AvailableUserIDs))
for _, robot := range result.Robots {
if !allowed[robot.UserID] {
continue
}
items = append(items, AvailableRoomRobot{
UserID: strconv.FormatInt(robot.UserID, 10),
Status: robot.Status,
RoomScene: robot.RoomScene,
LastUsedAtMS: robot.LastUsedAtMS,
UsedCount: robot.UsedCount,
User: owners[robot.UserID],
})
}
return items, nil
}
func (s *Service) CreateRobotRoom(ctx context.Context, req createRobotRoomRequest, actor shared.Actor) (RobotRoom, error) {
if s.roomClient == nil {
return RobotRoom{}, fmt.Errorf("room service client is not configured")
}
if _, err := normalizeCreateRobotRoomRequest(req); err != nil {
return RobotRoom{}, err
}
created, err := s.roomClient.CreateRobotRoom(ctx, roomclient.CreateRobotRoomRequest{
OwnerRobotUserID: req.OwnerRobotUserID,
CandidateRobotUserIDs: req.CandidateRobotUserIDs,
MinRobotCount: req.MinRobotCount,
MaxRobotCount: req.MaxRobotCount,
RoomName: req.RoomName,
RoomAvatar: req.RoomAvatar,
VisibleRegionID: req.VisibleRegionID,
GiftRule: roomclient.RobotRoomGiftRule{
GiftIDs: req.GiftIDs,
LuckyGiftIDs: req.LuckyGiftIDs,
NormalGiftIntervalMS: req.NormalGiftIntervalMS,
LuckyComboMin: req.LuckyComboMin,
LuckyComboMax: req.LuckyComboMax,
LuckyPauseMinMS: req.LuckyPauseMinMS,
LuckyPauseMaxMS: req.LuckyPauseMaxMS,
},
AdminID: uint64(actor.UserID),
})
if err != nil {
return RobotRoom{}, mapRoomClientError(err)
}
items := []RobotRoom{robotRoomFromClient(created)}
if err := s.fillRobotRoomDetails(ctx, items); err != nil {
return RobotRoom{}, err
}
return items[0], nil
}
func (s *Service) SetRobotRoomStatus(ctx context.Context, roomID string, status string, actor shared.Actor) (RobotRoom, error) {
if s.roomClient == nil {
return RobotRoom{}, fmt.Errorf("room service client is not configured")
}
roomID = strings.TrimSpace(roomID)
status = strings.TrimSpace(status)
if roomID == "" || (status != "active" && status != "stopped") {
return RobotRoom{}, fmt.Errorf("%w: 机器人房间状态参数不正确", ErrInvalidArgument)
}
item, err := s.roomClient.SetRobotRoomStatus(ctx, roomclient.SetRobotRoomStatusRequest{
RoomID: roomID,
Status: status,
AdminID: uint64(actor.UserID),
})
if err != nil {
return RobotRoom{}, mapRoomClientError(err)
}
items := []RobotRoom{robotRoomFromClient(item)}
if err := s.fillRobotRoomDetails(ctx, items); err != nil {
return RobotRoom{}, err
}
return items[0], nil
}
func normalizeCreateRobotRoomRequest(req createRobotRoomRequest) (createRobotRoomRequest, error) {
if req.OwnerRobotUserID <= 0 {
return req, fmt.Errorf("%w: 请选择房主机器人", ErrInvalidArgument)
}
if req.MinRobotCount <= 0 || req.MaxRobotCount < req.MinRobotCount {
return req, fmt.Errorf("%w: 机器人数量范围不正确", ErrInvalidArgument)
}
if len(req.CandidateRobotUserIDs) == 0 {
return req, fmt.Errorf("%w: 请选择候选机器人", ErrInvalidArgument)
}
if len(req.GiftIDs) == 0 {
return req, fmt.Errorf("%w: 请选择普通礼物合集", ErrInvalidArgument)
}
if len(req.LuckyGiftIDs) == 0 {
return req, fmt.Errorf("%w: 请选择幸运礼物合集", ErrInvalidArgument)
}
if req.NormalGiftIntervalMS <= 0 {
return req, fmt.Errorf("%w: 普通礼物频次不正确", ErrInvalidArgument)
}
if req.LuckyComboMin <= 0 || req.LuckyComboMax < req.LuckyComboMin {
return req, fmt.Errorf("%w: 幸运礼物连击范围不正确", ErrInvalidArgument)
}
if req.LuckyPauseMinMS <= 0 || req.LuckyPauseMaxMS < req.LuckyPauseMinMS {
return req, fmt.Errorf("%w: 幸运礼物间隔范围不正确", ErrInvalidArgument)
}
return req, nil
}
func robotRoomsFromClient(items []roomclient.RobotRoom) []RobotRoom {
out := make([]RobotRoom, 0, len(items))
for _, item := range items {
out = append(out, robotRoomFromClient(item))
}
return out
}
func robotRoomFromClient(item roomclient.RobotRoom) RobotRoom {
robotUserIDs := make([]string, 0, len(item.RobotUserIDs))
for _, userID := range item.RobotUserIDs {
robotUserIDs = append(robotUserIDs, strconv.FormatInt(userID, 10))
}
return RobotRoom{
AppCode: item.AppCode,
RoomID: item.RoomID,
RoomShortID: item.RoomShortID,
Title: item.Title,
CoverURL: item.CoverURL,
VisibleRegionID: item.VisibleRegionID,
Status: item.Status,
OwnerRobotUserID: strconv.FormatInt(item.OwnerRobotUserID, 10),
RobotUserIDs: robotUserIDs,
GiftRule: RobotGiftRule{
GiftIDs: item.GiftRule.GiftIDs,
LuckyGiftIDs: item.GiftRule.LuckyGiftIDs,
NormalGiftIntervalMS: item.GiftRule.NormalGiftIntervalMS,
LuckyComboMin: item.GiftRule.LuckyComboMin,
LuckyComboMax: item.GiftRule.LuckyComboMax,
LuckyPauseMinMS: item.GiftRule.LuckyPauseMinMS,
LuckyPauseMaxMS: item.GiftRule.LuckyPauseMaxMS,
},
CreatedByAdminID: item.CreatedByAdminID,
CreatedAtMS: item.CreatedAtMS,
UpdatedAtMS: item.UpdatedAtMS,
}
}
func (s *Service) fillRobotRoomDetails(ctx context.Context, items []RobotRoom) error {
if s.userDB == nil || len(items) == 0 {
return nil
}
userIDs := make([]int64, 0)
regionIDs := make([]int64, 0)
for _, item := range items {
if ownerID, err := strconv.ParseInt(item.OwnerRobotUserID, 10, 64); err == nil && ownerID > 0 {
userIDs = append(userIDs, ownerID)
}
for _, rawID := range item.RobotUserIDs {
if userID, err := strconv.ParseInt(rawID, 10, 64); err == nil && userID > 0 {
userIDs = append(userIDs, userID)
}
}
if item.VisibleRegionID > 0 {
regionIDs = append(regionIDs, item.VisibleRegionID)
}
}
owners, err := s.queryRoomOwners(ctx, uniqueInt64s(userIDs))
if err != nil {
return err
}
regions, err := s.queryRegionNames(ctx, uniqueInt64s(regionIDs))
if err != nil {
return err
}
for index := range items {
if ownerID, err := strconv.ParseInt(items[index].OwnerRobotUserID, 10, 64); err == nil {
items[index].Owner = owners[ownerID]
}
for _, rawID := range items[index].RobotUserIDs {
if userID, err := strconv.ParseInt(rawID, 10, 64); err == nil {
items[index].Robots = append(items[index].Robots, owners[userID])
}
}
items[index].RegionName = regions[items[index].VisibleRegionID]
}
return nil
}

View File

@ -15,6 +15,11 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.GET("/admin/rooms/pins", middleware.RequirePermission("room-pin:view"), h.ListRoomPins)
protected.POST("/admin/rooms/pins", middleware.RequirePermission("room-pin:create"), h.CreateRoomPin)
protected.DELETE("/admin/rooms/pins/:pin_id", middleware.RequirePermission("room-pin:cancel"), h.CancelRoomPin)
protected.GET("/admin/rooms/robot-rooms", middleware.RequirePermission("room-robot:view"), h.ListRobotRooms)
protected.GET("/admin/rooms/robot-rooms/available-robots", middleware.RequirePermission("room-robot:view"), h.ListAvailableRoomRobots)
protected.POST("/admin/rooms/robot-rooms", middleware.RequirePermission("room-robot:create"), h.CreateRobotRoom)
protected.POST("/admin/rooms/robot-rooms/:room_id/start", middleware.RequirePermission("room-robot:update"), h.StartRobotRoom)
protected.POST("/admin/rooms/robot-rooms/:room_id/stop", middleware.RequirePermission("room-robot:update"), h.StopRobotRoom)
protected.GET("/admin/rooms/config", middleware.RequirePermission("room-config:view"), h.GetRoomConfig)
protected.PUT("/admin/rooms/config", middleware.RequirePermission("room-config:update"), h.UpdateRoomConfig)
protected.PATCH("/admin/rooms/:room_id", middleware.RequirePermission("room:update"), h.UpdateRoom)

View File

@ -12,6 +12,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/robotclient"
"hyapp-admin-server/internal/integration/roomclient"
"hyapp-admin-server/internal/modules/shared"
)
@ -22,8 +23,9 @@ var (
)
type Service struct {
userDB *sql.DB
roomClient roomclient.Client
userDB *sql.DB
roomClient roomclient.Client
robotClient robotclient.Client
}
type RoomOwner struct {
@ -58,8 +60,8 @@ type Room struct {
VisibleRegionID int64 `json:"visibleRegionId"`
}
func NewService(userDB *sql.DB, roomClient roomclient.Client) *Service {
return &Service{userDB: userDB, roomClient: roomClient}
func NewService(userDB *sql.DB, roomClient roomclient.Client, robotClient robotclient.Client) *Service {
return &Service{userDB: userDB, roomClient: roomClient, robotClient: robotClient}
}
func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64, error) {

View File

@ -36,6 +36,9 @@ var defaultPermissions = []model.Permission{
{Name: "房间置顶取消", Code: "room-pin:cancel", Kind: "button"},
{Name: "房间配置查看", Code: "room-config:view", Kind: "menu"},
{Name: "房间配置更新", Code: "room-config:update", Kind: "button"},
{Name: "机器人房间查看", Code: "room-robot:view", Kind: "menu"},
{Name: "机器人房间创建", Code: "room-robot:create", Kind: "button"},
{Name: "机器人房间更新", Code: "room-robot:update", Kind: "button"},
{Name: "APP 配置查看", Code: "app-config:view", Kind: "menu"},
{Name: "APP 配置更新", Code: "app-config:update", Kind: "button"},
{Name: "版本管理查看", Code: "app-version:view", Kind: "menu"},
@ -98,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"},
@ -137,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"},
@ -255,6 +263,7 @@ func (s *Store) seedMenus() error {
{ParentID: &roomsID, Title: "房间列表", Code: "room-list", Path: "/rooms", Icon: "room", PermissionCode: "room:view", Sort: 65, Visible: true},
{ParentID: &roomsID, Title: "房间置顶", Code: "room-pins", Path: "/rooms/pins", Icon: "push_pin", PermissionCode: "room-pin:view", Sort: 66, Visible: true},
{ParentID: &roomsID, Title: "房间配置", Code: "room-config", Path: "/rooms/config", Icon: "settings", PermissionCode: "room-config:view", Sort: 67, Visible: true},
{ParentID: &roomsID, Title: "机器人房间", Code: "room-robots", Path: "/rooms/robots", Icon: "team", PermissionCode: "room-robot:view", Sort: 68, Visible: true},
{ParentID: &appConfigID, Title: "H5配置", Code: "app-config-h5", Path: "/app-config/h5", Icon: "settings", PermissionCode: "app-config:view", Sort: 66, Visible: true},
{ParentID: &appConfigID, Title: "BANNER配置", Code: "app-config-banners", Path: "/app-config/banners", Icon: "image", PermissionCode: "app-config:view", Sort: 67, Visible: true},
{ParentID: &appConfigID, Title: "开屏配置", Code: "app-config-splash-screens", Path: "/app-config/splash-screens", Icon: "image", PermissionCode: "app-config:view", Sort: 68, Visible: true},
@ -273,9 +282,10 @@ 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},
{ParentID: &paymentID, Title: "支付内购商品", Code: "payment-recharge-products", Path: "/payment/recharge-products", Icon: "wallet", PermissionCode: "payment-product:view", Sort: 70, Visible: true},
{ParentID: &activityID, Title: "每日任务", Code: "daily-task-list", Path: "/activities/daily-tasks", Icon: "task", PermissionCode: "daily-task:view", Sort: 69, Visible: true},
{ParentID: &activityID, Title: "注册奖励", Code: "registration-reward", Path: "/activities/registration-reward", Icon: "gift", PermissionCode: "registration-reward:view", Sort: 70, Visible: true},
{ParentID: &activityID, Title: "成就配置", Code: "achievement-config", Path: "/activities/achievements", Icon: "military_tech", PermissionCode: "achievement:view", Sort: 71, Visible: true},
@ -286,12 +296,13 @@ 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},
{ParentID: &geoID, Title: "国家管理", Code: "host-org-countries", Path: "/host/countries", Icon: "public", PermissionCode: "country:view", Sort: 70, Visible: true},
{ParentID: &geoID, Title: "区域管理", Code: "host-org-regions", Path: "/host/regions", Icon: "map", PermissionCode: "region:view", Sort: 71, Visible: true},
{ParentID: &hostOrgID, Title: "Manager列表", Code: "host-org-managers", Path: "/host/managers", Icon: "users", PermissionCode: "agency:view", Sort: 80, Visible: true},
{ParentID: &hostOrgID, Title: "经理列表", Code: "host-org-managers", Path: "/host/managers", Icon: "users", PermissionCode: "bd:view", Sort: 80, Visible: true},
{ParentID: &hostOrgID, Title: "Agency 列表", Code: "host-org-agencies", Path: "/host/agencies", Icon: "apartment", PermissionCode: "agency:view", Sort: 81, Visible: true},
{ParentID: &hostOrgID, Title: "BD Leader 列表", Code: "host-org-bd-leaders", Path: "/host/bd-leaders", Icon: "shield", PermissionCode: "bd:view", Sort: 82, Visible: true},
{ParentID: &hostOrgID, Title: "BD 列表", Code: "host-org-bds", Path: "/host/bds", Icon: "team", PermissionCode: "bd:view", Sort: 83, Visible: true},
@ -511,7 +522,7 @@ func defaultRolePermissionCodes(code string) []string {
"level-config:view", "level-config:update",
"pretty-id:view", "pretty-id:update", "pretty-id:generate", "pretty-id:grant",
"region-block:view", "region-block:update",
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update",
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update", "room-robot:view", "room-robot:create", "room-robot:update",
"app-config:view", "app-config:update",
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
"resource:view", "resource:create", "resource:update",
@ -526,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",
@ -537,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", "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",
@ -554,6 +566,7 @@ func defaultRolePermissionCodes(code string) []string {
"room:view",
"room-pin:view",
"room-config:view",
"room-robot:view",
"app-config:view",
"app-version:view",
"resource:view",
@ -576,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",
@ -589,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",
@ -605,7 +620,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
case "ops-admin":
return []string{
"app-user:update", "app-user:status", "app-user:password",
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update",
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update", "room-robot:view", "room-robot:create", "room-robot:update",
"app-config:view", "app-config:update",
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
"level-config:view", "level-config:update",
@ -622,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",
@ -633,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", "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
}

View File

@ -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)

View File

@ -0,0 +1,5 @@
SET @now_ms = UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000;
UPDATE admin_menus
SET title = '支付内购商品', updated_at_ms = @now_ms
WHERE code = 'payment-recharge-products';

View File

@ -0,0 +1,41 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
('机器人房间查看', 'room-robot:view', 'menu', '', @now_ms, @now_ms),
('机器人房间创建', 'room-robot:create', 'button', '', @now_ms, @now_ms),
('机器人房间更新', 'room-robot:update', 'button', '', @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
kind = VALUES(kind),
description = VALUES(description),
updated_at_ms = @now_ms;
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
SELECT parent.id, '机器人房间', 'room-robots', '/rooms/robots', 'team', 'room-robot:view', 68, TRUE, @now_ms, @now_ms
FROM admin_menus parent
WHERE parent.code = 'rooms'
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
title = VALUES(title),
path = VALUES(path),
icon = VALUES(icon),
permission_code = VALUES(permission_code),
sort = VALUES(sort),
visible = VALUES(visible),
updated_at_ms = @now_ms;
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code IN ('platform-admin', 'ops-admin')
AND admin_permission.code IN ('room-robot:view', 'room-robot:create', 'room-robot:update');
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code IN ('auditor', 'readonly')
AND admin_permission.code = 'room-robot:view';

View File

@ -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 '是否启用邀请活动',

View File

@ -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 周榜发奖事实表';

View File

@ -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='代理开业流水事件幂等表';

View File

@ -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))

View File

@ -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
}

View File

@ -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,

View File

@ -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),
}
}

View File

@ -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(),
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -323,6 +323,9 @@ func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.Ev
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
return broadcastdomain.ConsumeRoomEventResult{}, err
}
if gift.GetIsRobotGift() {
return result, nil
}
if gift.GetVisibleRegionId() <= 0 || !s.shouldBroadcastGift(&gift) {
// visible_region_id 来自房间/主播可见区域,不能用客户端当前 IP 或本地时区推断。
return result, nil

View File

@ -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)
}

View File

@ -257,6 +257,9 @@ func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.Ev
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
return 0, err
}
if gift.GetIsRobotGift() {
return 0, nil
}
if gift.GetSenderUserId() <= 0 || gift.GetTargetUserId() <= 0 || gift.GetGiftValue() <= 0 {
return 0, xerr.New(xerr.InvalidArgument, "room gift event is incomplete")
}

View 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 ""
}

View File

@ -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)

View File

@ -157,6 +157,9 @@ func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.Ev
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
return domain.EventResult{}, err
}
if gift.GetIsRobotGift() {
return domain.EventResult{EventID: envelope.GetEventId(), Status: domain.EventStatusSkipped}, nil
}
if strings.TrimSpace(envelope.GetRoomId()) == "" || gift.GetCoinSpent() <= 0 {
// 缺房间或 0 金币不会形成有效房间贡献;这类事件本身没有可补偿数据,跳过比失败重放更安全。
return domain.EventResult{EventID: envelope.GetEventId(), Status: domain.EventStatusSkipped}, nil

View File

@ -207,6 +207,9 @@ func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.Ev
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
return 0, err
}
if gift.GetIsRobotGift() {
return 0, nil
}
if gift.GetSenderUserId() <= 0 || gift.GetGiftId() == "" || gift.GetGiftCount() <= 0 {
return 0, nil
}

View File

@ -163,6 +163,9 @@ func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.Ev
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
return 0, err
}
if gift.GetIsRobotGift() {
return 0, nil
}
if gift.GetSenderUserId() <= 0 || gift.GetGiftId() == "" || gift.GetCoinSpent() <= 0 {
return 0, nil
}

View File

@ -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
}

View File

@ -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]
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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(),
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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,

View File

@ -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",

Some files were not shown because too many files have changed in this diff Show More