Merge branch 'main' into codex/admin-refresh-token-test

This commit is contained in:
zhx 2026-06-08 14:12:42 +08:00
commit 9c2413ca11
78 changed files with 21659 additions and 9280 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc-gen-go-grpc v1.5.1
// - protoc v7.35.0
// source: proto/activity/v1/activity.proto
@ -80,10 +80,10 @@ type ActivityServiceServer interface {
type UnimplementedActivityServiceServer struct{}
func (UnimplementedActivityServiceServer) PingActivity(context.Context, *PingActivityRequest) (*PingActivityResponse, error) {
return nil, status.Error(codes.Unimplemented, "method PingActivity not implemented")
return nil, status.Errorf(codes.Unimplemented, "method PingActivity not implemented")
}
func (UnimplementedActivityServiceServer) GetActivityStatus(context.Context, *GetActivityStatusRequest) (*GetActivityStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetActivityStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetActivityStatus not implemented")
}
func (UnimplementedActivityServiceServer) mustEmbedUnimplementedActivityServiceServer() {}
func (UnimplementedActivityServiceServer) testEmbeddedByValue() {}
@ -96,7 +96,7 @@ type UnsafeActivityServiceServer interface {
}
func RegisterActivityServiceServer(s grpc.ServiceRegistrar, srv ActivityServiceServer) {
// If the following call panics, it indicates UnimplementedActivityServiceServer was
// If the following call pancis, it indicates UnimplementedActivityServiceServer 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.
@ -289,25 +289,25 @@ type MessageInboxServiceServer interface {
type UnimplementedMessageInboxServiceServer struct{}
func (UnimplementedMessageInboxServiceServer) ListMessageTabs(context.Context, *ListMessageTabsRequest) (*ListMessageTabsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListMessageTabs not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListMessageTabs not implemented")
}
func (UnimplementedMessageInboxServiceServer) ListInboxMessages(context.Context, *ListInboxMessagesRequest) (*ListInboxMessagesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListInboxMessages not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListInboxMessages not implemented")
}
func (UnimplementedMessageInboxServiceServer) MarkInboxMessageRead(context.Context, *MarkInboxMessageReadRequest) (*MarkInboxMessageReadResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MarkInboxMessageRead not implemented")
return nil, status.Errorf(codes.Unimplemented, "method MarkInboxMessageRead not implemented")
}
func (UnimplementedMessageInboxServiceServer) MarkInboxSectionRead(context.Context, *MarkInboxSectionReadRequest) (*MarkInboxSectionReadResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MarkInboxSectionRead not implemented")
return nil, status.Errorf(codes.Unimplemented, "method MarkInboxSectionRead not implemented")
}
func (UnimplementedMessageInboxServiceServer) DeleteInboxMessage(context.Context, *DeleteInboxMessageRequest) (*DeleteInboxMessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteInboxMessage not implemented")
return nil, status.Errorf(codes.Unimplemented, "method DeleteInboxMessage not implemented")
}
func (UnimplementedMessageInboxServiceServer) CreateInboxMessage(context.Context, *CreateInboxMessageRequest) (*CreateInboxMessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateInboxMessage not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateInboxMessage not implemented")
}
func (UnimplementedMessageInboxServiceServer) CreateFanoutJob(context.Context, *CreateFanoutJobRequest) (*CreateFanoutJobResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateFanoutJob not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateFanoutJob not implemented")
}
func (UnimplementedMessageInboxServiceServer) mustEmbedUnimplementedMessageInboxServiceServer() {}
func (UnimplementedMessageInboxServiceServer) testEmbeddedByValue() {}
@ -320,7 +320,7 @@ type UnsafeMessageInboxServiceServer interface {
}
func RegisterMessageInboxServiceServer(s grpc.ServiceRegistrar, srv MessageInboxServiceServer) {
// If the following call panics, it indicates UnimplementedMessageInboxServiceServer was
// If the following call pancis, it indicates UnimplementedMessageInboxServiceServer 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.
@ -597,19 +597,19 @@ type ActivityCronServiceServer interface {
type UnimplementedActivityCronServiceServer struct{}
func (UnimplementedActivityCronServiceServer) ProcessMessageFanoutBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessMessageFanoutBatch not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessMessageFanoutBatch not implemented")
}
func (UnimplementedActivityCronServiceServer) ProcessLevelRewardBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessLevelRewardBatch not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessLevelRewardBatch not implemented")
}
func (UnimplementedActivityCronServiceServer) ProcessAchievementRewardBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessAchievementRewardBatch not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessAchievementRewardBatch not implemented")
}
func (UnimplementedActivityCronServiceServer) ProcessRoomTurnoverRewardSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessRoomTurnoverRewardSettlementBatch not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessRoomTurnoverRewardSettlementBatch not implemented")
}
func (UnimplementedActivityCronServiceServer) ProcessWeeklyStarSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessWeeklyStarSettlementBatch not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessWeeklyStarSettlementBatch not implemented")
}
func (UnimplementedActivityCronServiceServer) mustEmbedUnimplementedActivityCronServiceServer() {}
func (UnimplementedActivityCronServiceServer) testEmbeddedByValue() {}
@ -622,7 +622,7 @@ type UnsafeActivityCronServiceServer interface {
}
func RegisterActivityCronServiceServer(s grpc.ServiceRegistrar, srv ActivityCronServiceServer) {
// If the following call panics, it indicates UnimplementedActivityCronServiceServer was
// If the following call pancis, it indicates UnimplementedActivityCronServiceServer 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.
@ -829,13 +829,13 @@ type TaskServiceServer interface {
type UnimplementedTaskServiceServer struct{}
func (UnimplementedTaskServiceServer) ListUserTasks(context.Context, *ListUserTasksRequest) (*ListUserTasksResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListUserTasks not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListUserTasks not implemented")
}
func (UnimplementedTaskServiceServer) ClaimTaskReward(context.Context, *ClaimTaskRewardRequest) (*ClaimTaskRewardResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ClaimTaskReward not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ClaimTaskReward not implemented")
}
func (UnimplementedTaskServiceServer) ConsumeTaskEvent(context.Context, *ConsumeTaskEventRequest) (*ConsumeTaskEventResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ConsumeTaskEvent not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ConsumeTaskEvent not implemented")
}
func (UnimplementedTaskServiceServer) mustEmbedUnimplementedTaskServiceServer() {}
func (UnimplementedTaskServiceServer) testEmbeddedByValue() {}
@ -848,7 +848,7 @@ type UnsafeTaskServiceServer interface {
}
func RegisterTaskServiceServer(s grpc.ServiceRegistrar, srv TaskServiceServer) {
// If the following call panics, it indicates UnimplementedTaskServiceServer was
// If the following call pancis, it indicates UnimplementedTaskServiceServer 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.
@ -1050,22 +1050,22 @@ type GrowthLevelServiceServer interface {
type UnimplementedGrowthLevelServiceServer struct{}
func (UnimplementedGrowthLevelServiceServer) GetMyLevelOverview(context.Context, *GetMyLevelOverviewRequest) (*GetMyLevelOverviewResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetMyLevelOverview not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetMyLevelOverview not implemented")
}
func (UnimplementedGrowthLevelServiceServer) GetLevelTrack(context.Context, *GetLevelTrackRequest) (*GetLevelTrackResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetLevelTrack not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetLevelTrack not implemented")
}
func (UnimplementedGrowthLevelServiceServer) BatchGetUserLevelDisplayProfiles(context.Context, *BatchGetUserLevelDisplayProfilesRequest) (*BatchGetUserLevelDisplayProfilesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method BatchGetUserLevelDisplayProfiles not implemented")
return nil, status.Errorf(codes.Unimplemented, "method BatchGetUserLevelDisplayProfiles not implemented")
}
func (UnimplementedGrowthLevelServiceServer) ListLevelRewards(context.Context, *ListLevelRewardsRequest) (*ListLevelRewardsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListLevelRewards not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListLevelRewards not implemented")
}
func (UnimplementedGrowthLevelServiceServer) ConsumeLevelEvent(context.Context, *ConsumeLevelEventRequest) (*ConsumeLevelEventResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ConsumeLevelEvent not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ConsumeLevelEvent not implemented")
}
func (UnimplementedGrowthLevelServiceServer) IssueRegistrationLevelBadges(context.Context, *IssueRegistrationLevelBadgesRequest) (*IssueRegistrationLevelBadgesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method IssueRegistrationLevelBadges not implemented")
return nil, status.Errorf(codes.Unimplemented, "method IssueRegistrationLevelBadges not implemented")
}
func (UnimplementedGrowthLevelServiceServer) mustEmbedUnimplementedGrowthLevelServiceServer() {}
func (UnimplementedGrowthLevelServiceServer) testEmbeddedByValue() {}
@ -1078,7 +1078,7 @@ type UnsafeGrowthLevelServiceServer interface {
}
func RegisterGrowthLevelServiceServer(s grpc.ServiceRegistrar, srv GrowthLevelServiceServer) {
// If the following call panics, it indicates UnimplementedGrowthLevelServiceServer was
// If the following call pancis, it indicates UnimplementedGrowthLevelServiceServer 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.
@ -1320,16 +1320,16 @@ type AchievementServiceServer interface {
type UnimplementedAchievementServiceServer struct{}
func (UnimplementedAchievementServiceServer) ListAchievements(context.Context, *ListAchievementsRequest) (*ListAchievementsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListAchievements not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListAchievements not implemented")
}
func (UnimplementedAchievementServiceServer) ConsumeAchievementEvent(context.Context, *ConsumeAchievementEventRequest) (*ConsumeAchievementEventResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ConsumeAchievementEvent not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ConsumeAchievementEvent not implemented")
}
func (UnimplementedAchievementServiceServer) ListMyBadges(context.Context, *ListMyBadgesRequest) (*ListMyBadgesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListMyBadges not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListMyBadges not implemented")
}
func (UnimplementedAchievementServiceServer) SetBadgeDisplay(context.Context, *SetBadgeDisplayRequest) (*SetBadgeDisplayResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetBadgeDisplay not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetBadgeDisplay not implemented")
}
func (UnimplementedAchievementServiceServer) mustEmbedUnimplementedAchievementServiceServer() {}
func (UnimplementedAchievementServiceServer) testEmbeddedByValue() {}
@ -1342,7 +1342,7 @@ type UnsafeAchievementServiceServer interface {
}
func RegisterAchievementServiceServer(s grpc.ServiceRegistrar, srv AchievementServiceServer) {
// If the following call panics, it indicates UnimplementedAchievementServiceServer was
// If the following call pancis, it indicates UnimplementedAchievementServiceServer 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.
@ -1514,10 +1514,10 @@ type LuckyGiftServiceServer interface {
type UnimplementedLuckyGiftServiceServer struct{}
func (UnimplementedLuckyGiftServiceServer) CheckLuckyGift(context.Context, *CheckLuckyGiftRequest) (*CheckLuckyGiftResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CheckLuckyGift not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CheckLuckyGift not implemented")
}
func (UnimplementedLuckyGiftServiceServer) ExecuteLuckyGiftDraw(context.Context, *ExecuteLuckyGiftDrawRequest) (*ExecuteLuckyGiftDrawResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ExecuteLuckyGiftDraw not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ExecuteLuckyGiftDraw not implemented")
}
func (UnimplementedLuckyGiftServiceServer) mustEmbedUnimplementedLuckyGiftServiceServer() {}
func (UnimplementedLuckyGiftServiceServer) testEmbeddedByValue() {}
@ -1530,7 +1530,7 @@ type UnsafeLuckyGiftServiceServer interface {
}
func RegisterLuckyGiftServiceServer(s grpc.ServiceRegistrar, srv LuckyGiftServiceServer) {
// If the following call panics, it indicates UnimplementedLuckyGiftServiceServer was
// If the following call pancis, it indicates UnimplementedLuckyGiftServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -1645,7 +1645,7 @@ type RoomTurnoverRewardServiceServer interface {
type UnimplementedRoomTurnoverRewardServiceServer struct{}
func (UnimplementedRoomTurnoverRewardServiceServer) GetRoomTurnoverRewardStatus(context.Context, *GetRoomTurnoverRewardStatusRequest) (*GetRoomTurnoverRewardStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRoomTurnoverRewardStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetRoomTurnoverRewardStatus not implemented")
}
func (UnimplementedRoomTurnoverRewardServiceServer) mustEmbedUnimplementedRoomTurnoverRewardServiceServer() {
}
@ -1659,7 +1659,7 @@ type UnsafeRoomTurnoverRewardServiceServer interface {
}
func RegisterRoomTurnoverRewardServiceServer(s grpc.ServiceRegistrar, srv RoomTurnoverRewardServiceServer) {
// If the following call panics, it indicates UnimplementedRoomTurnoverRewardServiceServer was
// If the following call pancis, it indicates UnimplementedRoomTurnoverRewardServiceServer 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.
@ -1778,13 +1778,13 @@ type WeeklyStarServiceServer interface {
type UnimplementedWeeklyStarServiceServer struct{}
func (UnimplementedWeeklyStarServiceServer) GetWeeklyStarCurrent(context.Context, *GetWeeklyStarCurrentRequest) (*GetWeeklyStarCurrentResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetWeeklyStarCurrent not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetWeeklyStarCurrent not implemented")
}
func (UnimplementedWeeklyStarServiceServer) ListWeeklyStarLeaderboard(context.Context, *ListWeeklyStarLeaderboardRequest) (*ListWeeklyStarLeaderboardResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListWeeklyStarLeaderboard not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListWeeklyStarLeaderboard not implemented")
}
func (UnimplementedWeeklyStarServiceServer) ListWeeklyStarHistory(context.Context, *ListWeeklyStarHistoryRequest) (*ListWeeklyStarHistoryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListWeeklyStarHistory not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListWeeklyStarHistory not implemented")
}
func (UnimplementedWeeklyStarServiceServer) mustEmbedUnimplementedWeeklyStarServiceServer() {}
func (UnimplementedWeeklyStarServiceServer) testEmbeddedByValue() {}
@ -1797,7 +1797,7 @@ type UnsafeWeeklyStarServiceServer interface {
}
func RegisterWeeklyStarServiceServer(s grpc.ServiceRegistrar, srv WeeklyStarServiceServer) {
// If the following call panics, it indicates UnimplementedWeeklyStarServiceServer was
// If the following call pancis, it indicates UnimplementedWeeklyStarServiceServer 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.
@ -1986,19 +1986,19 @@ type BroadcastServiceServer interface {
type UnimplementedBroadcastServiceServer struct{}
func (UnimplementedBroadcastServiceServer) EnsureBroadcastGroups(context.Context, *EnsureBroadcastGroupsRequest) (*EnsureBroadcastGroupsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method EnsureBroadcastGroups not implemented")
return nil, status.Errorf(codes.Unimplemented, "method EnsureBroadcastGroups not implemented")
}
func (UnimplementedBroadcastServiceServer) PublishRegionBroadcast(context.Context, *PublishRegionBroadcastRequest) (*PublishBroadcastResponse, error) {
return nil, status.Error(codes.Unimplemented, "method PublishRegionBroadcast not implemented")
return nil, status.Errorf(codes.Unimplemented, "method PublishRegionBroadcast not implemented")
}
func (UnimplementedBroadcastServiceServer) PublishGlobalBroadcast(context.Context, *PublishGlobalBroadcastRequest) (*PublishBroadcastResponse, error) {
return nil, status.Error(codes.Unimplemented, "method PublishGlobalBroadcast not implemented")
return nil, status.Errorf(codes.Unimplemented, "method PublishGlobalBroadcast not implemented")
}
func (UnimplementedBroadcastServiceServer) RemoveRegionBroadcastMember(context.Context, *RemoveRegionBroadcastMemberRequest) (*RemoveRegionBroadcastMemberResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RemoveRegionBroadcastMember not implemented")
return nil, status.Errorf(codes.Unimplemented, "method RemoveRegionBroadcastMember not implemented")
}
func (UnimplementedBroadcastServiceServer) ProcessBroadcastOutboxBatch(context.Context, *CronBatchRequest) (*ProcessBroadcastOutboxBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessBroadcastOutboxBatch not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessBroadcastOutboxBatch not implemented")
}
func (UnimplementedBroadcastServiceServer) mustEmbedUnimplementedBroadcastServiceServer() {}
func (UnimplementedBroadcastServiceServer) testEmbeddedByValue() {}
@ -2011,7 +2011,7 @@ type UnsafeBroadcastServiceServer interface {
}
func RegisterBroadcastServiceServer(s grpc.ServiceRegistrar, srv BroadcastServiceServer) {
// If the following call panics, it indicates UnimplementedBroadcastServiceServer was
// If the following call pancis, it indicates UnimplementedBroadcastServiceServer 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.
@ -2192,7 +2192,7 @@ type RoomEventConsumerServiceServer interface {
type UnimplementedRoomEventConsumerServiceServer struct{}
func (UnimplementedRoomEventConsumerServiceServer) ConsumeRoomEvent(context.Context, *ConsumeRoomEventRequest) (*ConsumeRoomEventResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ConsumeRoomEvent not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ConsumeRoomEvent not implemented")
}
func (UnimplementedRoomEventConsumerServiceServer) mustEmbedUnimplementedRoomEventConsumerServiceServer() {
}
@ -2206,7 +2206,7 @@ type UnsafeRoomEventConsumerServiceServer interface {
}
func RegisterRoomEventConsumerServiceServer(s grpc.ServiceRegistrar, srv RoomEventConsumerServiceServer) {
// If the following call panics, it indicates UnimplementedRoomEventConsumerServiceServer was
// If the following call pancis, it indicates UnimplementedRoomEventConsumerServiceServer 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.
@ -2325,13 +2325,13 @@ type AdminTaskServiceServer interface {
type UnimplementedAdminTaskServiceServer struct{}
func (UnimplementedAdminTaskServiceServer) ListTaskDefinitions(context.Context, *ListTaskDefinitionsRequest) (*ListTaskDefinitionsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListTaskDefinitions not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListTaskDefinitions not implemented")
}
func (UnimplementedAdminTaskServiceServer) UpsertTaskDefinition(context.Context, *UpsertTaskDefinitionRequest) (*UpsertTaskDefinitionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpsertTaskDefinition not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpsertTaskDefinition not implemented")
}
func (UnimplementedAdminTaskServiceServer) SetTaskDefinitionStatus(context.Context, *SetTaskDefinitionStatusRequest) (*SetTaskDefinitionStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetTaskDefinitionStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetTaskDefinitionStatus not implemented")
}
func (UnimplementedAdminTaskServiceServer) mustEmbedUnimplementedAdminTaskServiceServer() {}
func (UnimplementedAdminTaskServiceServer) testEmbeddedByValue() {}
@ -2344,7 +2344,7 @@ type UnsafeAdminTaskServiceServer interface {
}
func RegisterAdminTaskServiceServer(s grpc.ServiceRegistrar, srv AdminTaskServiceServer) {
// If the following call panics, it indicates UnimplementedAdminTaskServiceServer was
// If the following call pancis, it indicates UnimplementedAdminTaskServiceServer 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.
@ -2494,10 +2494,10 @@ type RegistrationRewardServiceServer interface {
type UnimplementedRegistrationRewardServiceServer struct{}
func (UnimplementedRegistrationRewardServiceServer) GetRegistrationRewardEligibility(context.Context, *GetRegistrationRewardEligibilityRequest) (*GetRegistrationRewardEligibilityResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRegistrationRewardEligibility not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetRegistrationRewardEligibility not implemented")
}
func (UnimplementedRegistrationRewardServiceServer) IssueRegistrationReward(context.Context, *IssueRegistrationRewardRequest) (*IssueRegistrationRewardResponse, error) {
return nil, status.Error(codes.Unimplemented, "method IssueRegistrationReward not implemented")
return nil, status.Errorf(codes.Unimplemented, "method IssueRegistrationReward not implemented")
}
func (UnimplementedRegistrationRewardServiceServer) mustEmbedUnimplementedRegistrationRewardServiceServer() {
}
@ -2511,7 +2511,7 @@ type UnsafeRegistrationRewardServiceServer interface {
}
func RegisterRegistrationRewardServiceServer(s grpc.ServiceRegistrar, srv RegistrationRewardServiceServer) {
// If the following call panics, it indicates UnimplementedRegistrationRewardServiceServer was
// If the following call pancis, it indicates UnimplementedRegistrationRewardServiceServer 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.
@ -2652,13 +2652,13 @@ type AdminRegistrationRewardServiceServer interface {
type UnimplementedAdminRegistrationRewardServiceServer struct{}
func (UnimplementedAdminRegistrationRewardServiceServer) GetRegistrationRewardConfig(context.Context, *GetRegistrationRewardConfigRequest) (*GetRegistrationRewardConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRegistrationRewardConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetRegistrationRewardConfig not implemented")
}
func (UnimplementedAdminRegistrationRewardServiceServer) UpdateRegistrationRewardConfig(context.Context, *UpdateRegistrationRewardConfigRequest) (*UpdateRegistrationRewardConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateRegistrationRewardConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateRegistrationRewardConfig not implemented")
}
func (UnimplementedAdminRegistrationRewardServiceServer) ListRegistrationRewardClaims(context.Context, *ListRegistrationRewardClaimsRequest) (*ListRegistrationRewardClaimsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRegistrationRewardClaims not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRegistrationRewardClaims not implemented")
}
func (UnimplementedAdminRegistrationRewardServiceServer) mustEmbedUnimplementedAdminRegistrationRewardServiceServer() {
}
@ -2672,7 +2672,7 @@ type UnsafeAdminRegistrationRewardServiceServer interface {
}
func RegisterAdminRegistrationRewardServiceServer(s grpc.ServiceRegistrar, srv AdminRegistrationRewardServiceServer) {
// If the following call panics, it indicates UnimplementedAdminRegistrationRewardServiceServer was
// If the following call pancis, it indicates UnimplementedAdminRegistrationRewardServiceServer 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.
@ -2822,10 +2822,10 @@ type FirstRechargeRewardServiceServer interface {
type UnimplementedFirstRechargeRewardServiceServer struct{}
func (UnimplementedFirstRechargeRewardServiceServer) GetFirstRechargeRewardStatus(context.Context, *GetFirstRechargeRewardStatusRequest) (*GetFirstRechargeRewardStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetFirstRechargeRewardStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetFirstRechargeRewardStatus not implemented")
}
func (UnimplementedFirstRechargeRewardServiceServer) ConsumeFirstRechargeReward(context.Context, *ConsumeFirstRechargeRewardRequest) (*ConsumeFirstRechargeRewardResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ConsumeFirstRechargeReward not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ConsumeFirstRechargeReward not implemented")
}
func (UnimplementedFirstRechargeRewardServiceServer) mustEmbedUnimplementedFirstRechargeRewardServiceServer() {
}
@ -2839,7 +2839,7 @@ type UnsafeFirstRechargeRewardServiceServer interface {
}
func RegisterFirstRechargeRewardServiceServer(s grpc.ServiceRegistrar, srv FirstRechargeRewardServiceServer) {
// If the following call panics, it indicates UnimplementedFirstRechargeRewardServiceServer was
// If the following call pancis, it indicates UnimplementedFirstRechargeRewardServiceServer 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.
@ -2980,13 +2980,13 @@ type AdminFirstRechargeRewardServiceServer interface {
type UnimplementedAdminFirstRechargeRewardServiceServer struct{}
func (UnimplementedAdminFirstRechargeRewardServiceServer) GetFirstRechargeRewardConfig(context.Context, *GetFirstRechargeRewardConfigRequest) (*GetFirstRechargeRewardConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetFirstRechargeRewardConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetFirstRechargeRewardConfig not implemented")
}
func (UnimplementedAdminFirstRechargeRewardServiceServer) UpdateFirstRechargeRewardConfig(context.Context, *UpdateFirstRechargeRewardConfigRequest) (*UpdateFirstRechargeRewardConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateFirstRechargeRewardConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateFirstRechargeRewardConfig not implemented")
}
func (UnimplementedAdminFirstRechargeRewardServiceServer) ListFirstRechargeRewardClaims(context.Context, *ListFirstRechargeRewardClaimsRequest) (*ListFirstRechargeRewardClaimsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListFirstRechargeRewardClaims not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListFirstRechargeRewardClaims not implemented")
}
func (UnimplementedAdminFirstRechargeRewardServiceServer) mustEmbedUnimplementedAdminFirstRechargeRewardServiceServer() {
}
@ -3000,7 +3000,7 @@ type UnsafeAdminFirstRechargeRewardServiceServer interface {
}
func RegisterAdminFirstRechargeRewardServiceServer(s grpc.ServiceRegistrar, srv AdminFirstRechargeRewardServiceServer) {
// If the following call panics, it indicates UnimplementedAdminFirstRechargeRewardServiceServer was
// If the following call pancis, it indicates UnimplementedAdminFirstRechargeRewardServiceServer 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.
@ -3150,10 +3150,10 @@ type CumulativeRechargeRewardServiceServer interface {
type UnimplementedCumulativeRechargeRewardServiceServer struct{}
func (UnimplementedCumulativeRechargeRewardServiceServer) GetCumulativeRechargeRewardStatus(context.Context, *GetCumulativeRechargeRewardStatusRequest) (*GetCumulativeRechargeRewardStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetCumulativeRechargeRewardStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetCumulativeRechargeRewardStatus not implemented")
}
func (UnimplementedCumulativeRechargeRewardServiceServer) ConsumeCumulativeRechargeReward(context.Context, *ConsumeCumulativeRechargeRewardRequest) (*ConsumeCumulativeRechargeRewardResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ConsumeCumulativeRechargeReward not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ConsumeCumulativeRechargeReward not implemented")
}
func (UnimplementedCumulativeRechargeRewardServiceServer) mustEmbedUnimplementedCumulativeRechargeRewardServiceServer() {
}
@ -3167,7 +3167,7 @@ type UnsafeCumulativeRechargeRewardServiceServer interface {
}
func RegisterCumulativeRechargeRewardServiceServer(s grpc.ServiceRegistrar, srv CumulativeRechargeRewardServiceServer) {
// If the following call panics, it indicates UnimplementedCumulativeRechargeRewardServiceServer was
// If the following call pancis, it indicates UnimplementedCumulativeRechargeRewardServiceServer 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.
@ -3308,13 +3308,13 @@ type AdminCumulativeRechargeRewardServiceServer interface {
type UnimplementedAdminCumulativeRechargeRewardServiceServer struct{}
func (UnimplementedAdminCumulativeRechargeRewardServiceServer) GetCumulativeRechargeRewardConfig(context.Context, *GetCumulativeRechargeRewardConfigRequest) (*GetCumulativeRechargeRewardConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetCumulativeRechargeRewardConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetCumulativeRechargeRewardConfig not implemented")
}
func (UnimplementedAdminCumulativeRechargeRewardServiceServer) UpdateCumulativeRechargeRewardConfig(context.Context, *UpdateCumulativeRechargeRewardConfigRequest) (*UpdateCumulativeRechargeRewardConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateCumulativeRechargeRewardConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateCumulativeRechargeRewardConfig not implemented")
}
func (UnimplementedAdminCumulativeRechargeRewardServiceServer) ListCumulativeRechargeRewardGrants(context.Context, *ListCumulativeRechargeRewardGrantsRequest) (*ListCumulativeRechargeRewardGrantsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListCumulativeRechargeRewardGrants not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListCumulativeRechargeRewardGrants not implemented")
}
func (UnimplementedAdminCumulativeRechargeRewardServiceServer) mustEmbedUnimplementedAdminCumulativeRechargeRewardServiceServer() {
}
@ -3328,7 +3328,7 @@ type UnsafeAdminCumulativeRechargeRewardServiceServer interface {
}
func RegisterAdminCumulativeRechargeRewardServiceServer(s grpc.ServiceRegistrar, srv AdminCumulativeRechargeRewardServiceServer) {
// If the following call panics, it indicates UnimplementedAdminCumulativeRechargeRewardServiceServer was
// If the following call pancis, it indicates UnimplementedAdminCumulativeRechargeRewardServiceServer 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.
@ -3504,16 +3504,16 @@ type AdminRoomTurnoverRewardServiceServer interface {
type UnimplementedAdminRoomTurnoverRewardServiceServer struct{}
func (UnimplementedAdminRoomTurnoverRewardServiceServer) GetRoomTurnoverRewardConfig(context.Context, *GetRoomTurnoverRewardConfigRequest) (*GetRoomTurnoverRewardConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRoomTurnoverRewardConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetRoomTurnoverRewardConfig not implemented")
}
func (UnimplementedAdminRoomTurnoverRewardServiceServer) UpdateRoomTurnoverRewardConfig(context.Context, *UpdateRoomTurnoverRewardConfigRequest) (*UpdateRoomTurnoverRewardConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateRoomTurnoverRewardConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateRoomTurnoverRewardConfig not implemented")
}
func (UnimplementedAdminRoomTurnoverRewardServiceServer) ListRoomTurnoverRewardSettlements(context.Context, *ListRoomTurnoverRewardSettlementsRequest) (*ListRoomTurnoverRewardSettlementsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomTurnoverRewardSettlements not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRoomTurnoverRewardSettlements not implemented")
}
func (UnimplementedAdminRoomTurnoverRewardServiceServer) RetryRoomTurnoverRewardSettlement(context.Context, *RetryRoomTurnoverRewardSettlementRequest) (*RetryRoomTurnoverRewardSettlementResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RetryRoomTurnoverRewardSettlement not implemented")
return nil, status.Errorf(codes.Unimplemented, "method RetryRoomTurnoverRewardSettlement not implemented")
}
func (UnimplementedAdminRoomTurnoverRewardServiceServer) mustEmbedUnimplementedAdminRoomTurnoverRewardServiceServer() {
}
@ -3527,7 +3527,7 @@ type UnsafeAdminRoomTurnoverRewardServiceServer interface {
}
func RegisterAdminRoomTurnoverRewardServiceServer(s grpc.ServiceRegistrar, srv AdminRoomTurnoverRewardServiceServer) {
// If the following call panics, it indicates UnimplementedAdminRoomTurnoverRewardServiceServer was
// If the following call pancis, it indicates UnimplementedAdminRoomTurnoverRewardServiceServer 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.
@ -3764,25 +3764,25 @@ type AdminWeeklyStarServiceServer interface {
type UnimplementedAdminWeeklyStarServiceServer struct{}
func (UnimplementedAdminWeeklyStarServiceServer) ListWeeklyStarCycles(context.Context, *ListWeeklyStarCyclesRequest) (*ListWeeklyStarCyclesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListWeeklyStarCycles not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListWeeklyStarCycles not implemented")
}
func (UnimplementedAdminWeeklyStarServiceServer) CreateWeeklyStarCycle(context.Context, *UpsertWeeklyStarCycleRequest) (*UpsertWeeklyStarCycleResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateWeeklyStarCycle not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateWeeklyStarCycle not implemented")
}
func (UnimplementedAdminWeeklyStarServiceServer) GetWeeklyStarCycle(context.Context, *GetWeeklyStarCycleRequest) (*GetWeeklyStarCycleResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetWeeklyStarCycle not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetWeeklyStarCycle not implemented")
}
func (UnimplementedAdminWeeklyStarServiceServer) UpdateWeeklyStarCycle(context.Context, *UpsertWeeklyStarCycleRequest) (*UpsertWeeklyStarCycleResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateWeeklyStarCycle not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateWeeklyStarCycle not implemented")
}
func (UnimplementedAdminWeeklyStarServiceServer) SetWeeklyStarCycleStatus(context.Context, *SetWeeklyStarCycleStatusRequest) (*SetWeeklyStarCycleStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetWeeklyStarCycleStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetWeeklyStarCycleStatus not implemented")
}
func (UnimplementedAdminWeeklyStarServiceServer) ListWeeklyStarLeaderboard(context.Context, *ListWeeklyStarLeaderboardRequest) (*ListWeeklyStarLeaderboardResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListWeeklyStarLeaderboard not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListWeeklyStarLeaderboard not implemented")
}
func (UnimplementedAdminWeeklyStarServiceServer) ListWeeklyStarSettlements(context.Context, *ListWeeklyStarSettlementsRequest) (*ListWeeklyStarSettlementsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListWeeklyStarSettlements not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListWeeklyStarSettlements not implemented")
}
func (UnimplementedAdminWeeklyStarServiceServer) mustEmbedUnimplementedAdminWeeklyStarServiceServer() {
}
@ -3796,7 +3796,7 @@ type UnsafeAdminWeeklyStarServiceServer interface {
}
func RegisterAdminWeeklyStarServiceServer(s grpc.ServiceRegistrar, srv AdminWeeklyStarServiceServer) {
// If the following call panics, it indicates UnimplementedAdminWeeklyStarServiceServer was
// If the following call pancis, it indicates UnimplementedAdminWeeklyStarServiceServer 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.
@ -4034,10 +4034,10 @@ type SevenDayCheckInServiceServer interface {
type UnimplementedSevenDayCheckInServiceServer struct{}
func (UnimplementedSevenDayCheckInServiceServer) GetSevenDayCheckInStatus(context.Context, *GetSevenDayCheckInStatusRequest) (*GetSevenDayCheckInStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetSevenDayCheckInStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetSevenDayCheckInStatus not implemented")
}
func (UnimplementedSevenDayCheckInServiceServer) SignSevenDayCheckIn(context.Context, *SignSevenDayCheckInRequest) (*SignSevenDayCheckInResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SignSevenDayCheckIn not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SignSevenDayCheckIn not implemented")
}
func (UnimplementedSevenDayCheckInServiceServer) mustEmbedUnimplementedSevenDayCheckInServiceServer() {
}
@ -4051,7 +4051,7 @@ type UnsafeSevenDayCheckInServiceServer interface {
}
func RegisterSevenDayCheckInServiceServer(s grpc.ServiceRegistrar, srv SevenDayCheckInServiceServer) {
// If the following call panics, it indicates UnimplementedSevenDayCheckInServiceServer was
// If the following call pancis, it indicates UnimplementedSevenDayCheckInServiceServer 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.
@ -4192,13 +4192,13 @@ type AdminSevenDayCheckInServiceServer interface {
type UnimplementedAdminSevenDayCheckInServiceServer struct{}
func (UnimplementedAdminSevenDayCheckInServiceServer) GetSevenDayCheckInConfig(context.Context, *GetSevenDayCheckInConfigRequest) (*GetSevenDayCheckInConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetSevenDayCheckInConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetSevenDayCheckInConfig not implemented")
}
func (UnimplementedAdminSevenDayCheckInServiceServer) UpdateSevenDayCheckInConfig(context.Context, *UpdateSevenDayCheckInConfigRequest) (*UpdateSevenDayCheckInConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateSevenDayCheckInConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateSevenDayCheckInConfig not implemented")
}
func (UnimplementedAdminSevenDayCheckInServiceServer) ListSevenDayCheckInClaims(context.Context, *ListSevenDayCheckInClaimsRequest) (*ListSevenDayCheckInClaimsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListSevenDayCheckInClaims not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListSevenDayCheckInClaims not implemented")
}
func (UnimplementedAdminSevenDayCheckInServiceServer) mustEmbedUnimplementedAdminSevenDayCheckInServiceServer() {
}
@ -4212,7 +4212,7 @@ type UnsafeAdminSevenDayCheckInServiceServer interface {
}
func RegisterAdminSevenDayCheckInServiceServer(s grpc.ServiceRegistrar, srv AdminSevenDayCheckInServiceServer) {
// If the following call panics, it indicates UnimplementedAdminSevenDayCheckInServiceServer was
// If the following call pancis, it indicates UnimplementedAdminSevenDayCheckInServiceServer 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.
@ -4388,16 +4388,16 @@ type AdminGrowthLevelServiceServer interface {
type UnimplementedAdminGrowthLevelServiceServer struct{}
func (UnimplementedAdminGrowthLevelServiceServer) ListLevelConfig(context.Context, *ListLevelConfigRequest) (*ListLevelConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListLevelConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListLevelConfig not implemented")
}
func (UnimplementedAdminGrowthLevelServiceServer) UpsertLevelTrack(context.Context, *UpsertLevelTrackRequest) (*UpsertLevelTrackResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpsertLevelTrack not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpsertLevelTrack not implemented")
}
func (UnimplementedAdminGrowthLevelServiceServer) UpsertLevelRule(context.Context, *UpsertLevelRuleRequest) (*UpsertLevelRuleResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpsertLevelRule not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpsertLevelRule not implemented")
}
func (UnimplementedAdminGrowthLevelServiceServer) UpsertLevelTier(context.Context, *UpsertLevelTierRequest) (*UpsertLevelTierResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpsertLevelTier not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpsertLevelTier not implemented")
}
func (UnimplementedAdminGrowthLevelServiceServer) mustEmbedUnimplementedAdminGrowthLevelServiceServer() {
}
@ -4411,7 +4411,7 @@ type UnsafeAdminGrowthLevelServiceServer interface {
}
func RegisterAdminGrowthLevelServiceServer(s grpc.ServiceRegistrar, srv AdminGrowthLevelServiceServer) {
// If the following call panics, it indicates UnimplementedAdminGrowthLevelServiceServer was
// If the following call pancis, it indicates UnimplementedAdminGrowthLevelServiceServer 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.
@ -4583,10 +4583,10 @@ type AdminAchievementServiceServer interface {
type UnimplementedAdminAchievementServiceServer struct{}
func (UnimplementedAdminAchievementServiceServer) ListAchievementDefinitions(context.Context, *ListAchievementsRequest) (*ListAchievementsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListAchievementDefinitions not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListAchievementDefinitions not implemented")
}
func (UnimplementedAdminAchievementServiceServer) UpsertAchievementDefinition(context.Context, *UpsertAchievementDefinitionRequest) (*UpsertAchievementDefinitionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpsertAchievementDefinition not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpsertAchievementDefinition not implemented")
}
func (UnimplementedAdminAchievementServiceServer) mustEmbedUnimplementedAdminAchievementServiceServer() {
}
@ -4600,7 +4600,7 @@ type UnsafeAdminAchievementServiceServer interface {
}
func RegisterAdminAchievementServiceServer(s grpc.ServiceRegistrar, srv AdminAchievementServiceServer) {
// If the following call panics, it indicates UnimplementedAdminAchievementServiceServer was
// If the following call pancis, it indicates UnimplementedAdminAchievementServiceServer 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.
@ -4767,19 +4767,19 @@ type AdminLuckyGiftServiceServer interface {
type UnimplementedAdminLuckyGiftServiceServer struct{}
func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftConfig(context.Context, *GetLuckyGiftConfigRequest) (*GetLuckyGiftConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetLuckyGiftConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetLuckyGiftConfig not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) UpsertLuckyGiftConfig(context.Context, *UpsertLuckyGiftConfigRequest) (*UpsertLuckyGiftConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpsertLuckyGiftConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpsertLuckyGiftConfig not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftConfigs not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListLuckyGiftConfigs not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftDraws not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListLuckyGiftDraws not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetLuckyGiftDrawSummary not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetLuckyGiftDrawSummary not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) mustEmbedUnimplementedAdminLuckyGiftServiceServer() {}
func (UnimplementedAdminLuckyGiftServiceServer) testEmbeddedByValue() {}
@ -4792,7 +4792,7 @@ type UnsafeAdminLuckyGiftServiceServer interface {
}
func RegisterAdminLuckyGiftServiceServer(s grpc.ServiceRegistrar, srv AdminLuckyGiftServiceServer) {
// If the following call panics, it indicates UnimplementedAdminLuckyGiftServiceServer was
// If the following call pancis, it indicates UnimplementedAdminLuckyGiftServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.

View File

@ -1212,6 +1212,11 @@ type RoomGiftSent struct {
CoinSpent int64 `protobuf:"varint,10,opt,name=coin_spent,json=coinSpent,proto3" json:"coin_spent,omitempty"`
CountryId int64 `protobuf:"varint,11,opt,name=country_id,json=countryId,proto3" json:"country_id,omitempty"`
RegionId int64 `protobuf:"varint,12,opt,name=region_id,json=regionId,proto3" json:"region_id,omitempty"`
GiftTypeCode string `protobuf:"bytes,13,opt,name=gift_type_code,json=giftTypeCode,proto3" json:"gift_type_code,omitempty"`
CpRelationType string `protobuf:"bytes,14,opt,name=cp_relation_type,json=cpRelationType,proto3" json:"cp_relation_type,omitempty"`
GiftName string `protobuf:"bytes,15,opt,name=gift_name,json=giftName,proto3" json:"gift_name,omitempty"`
GiftIconUrl string `protobuf:"bytes,16,opt,name=gift_icon_url,json=giftIconUrl,proto3" json:"gift_icon_url,omitempty"`
GiftAnimationUrl string `protobuf:"bytes,17,opt,name=gift_animation_url,json=giftAnimationUrl,proto3" json:"gift_animation_url,omitempty"`
}
func (x *RoomGiftSent) Reset() {
@ -1328,6 +1333,41 @@ func (x *RoomGiftSent) GetRegionId() int64 {
return 0
}
func (x *RoomGiftSent) GetGiftTypeCode() string {
if x != nil {
return x.GiftTypeCode
}
return ""
}
func (x *RoomGiftSent) GetCpRelationType() string {
if x != nil {
return x.CpRelationType
}
return ""
}
func (x *RoomGiftSent) GetGiftName() string {
if x != nil {
return x.GiftName
}
return ""
}
func (x *RoomGiftSent) GetGiftIconUrl() string {
if x != nil {
return x.GiftIconUrl
}
return ""
}
func (x *RoomGiftSent) GetGiftAnimationUrl() string {
if x != nil {
return x.GiftAnimationUrl
}
return ""
}
// RoomHeatChanged 表达热度变化结果。
type RoomHeatChanged struct {
state protoimpl.MessageState
@ -2191,7 +2231,7 @@ var file_proto_events_room_v1_events_proto_rawDesc = []byte{
0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65,
0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72,
0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x9e, 0x03, 0x0a, 0x0c, 0x52, 0x6f,
0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xdd, 0x04, 0x0a, 0x0c, 0x52, 0x6f,
0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65,
0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64,
@ -2217,134 +2257,146 @@ var file_proto_events_room_v1_events_proto_rawDesc = []byte{
0x0a, 0x0a, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01,
0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x1b, 0x0a,
0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03,
0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x4a, 0x0a, 0x0f, 0x52, 0x6f,
0x6f, 0x6d, 0x48, 0x65, 0x61, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x14, 0x0a,
0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x64, 0x65,
0x6c, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x68,
0x65, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65,
0x6e, 0x74, 0x48, 0x65, 0x61, 0x74, 0x22, 0x5f, 0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x61,
0x6e, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65,
0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72,
0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x03, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74,
0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67, 0x69,
0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x94, 0x02, 0x0a, 0x15, 0x52, 0x6f, 0x6f, 0x6d,
0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e,
0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x72, 0x6f, 0x6c, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x52, 0x6f,
0x6c, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20,
0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x72,
0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x49, 0x74, 0x65, 0x6d, 0x49,
0x64, 0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x67, 0x72,
0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x21, 0x0a,
0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65,
0x12, 0x19, 0x0a, 0x08, 0x69, 0x63, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01,
0x28, 0x09, 0x52, 0x07, 0x69, 0x63, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x67,
0x72, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67,
0x72, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xeb,
0x03, 0x0a, 0x15, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x46, 0x75, 0x65,
0x6c, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b,
0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63,
0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02,
0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x61,
0x64, 0x64, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
0x09, 0x61, 0x64, 0x64, 0x65, 0x64, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x30, 0x0a, 0x14, 0x65, 0x66,
0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x66, 0x75,
0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74,
0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x65, 0x64, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x23, 0x0a, 0x0d,
0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x05, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x46, 0x75, 0x65,
0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x75, 0x65,
0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74,
0x46, 0x75, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x75, 0x65, 0x6c, 0x5f, 0x74, 0x68, 0x72,
0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x66, 0x75,
0x65, 0x6c, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73,
0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x74, 0x5f,
0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73, 0x65, 0x74, 0x41,
0x74, 0x4d, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e,
0x64, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x69, 0x66,
0x74, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69, 0x66, 0x74,
0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x75, 0x6e,
0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18,
0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64,
0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69,
0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73,
0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x82, 0x04, 0x0a,
0x11, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x67, 0x6e, 0x69, 0x74,
0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12,
0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05,
0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74,
0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72,
0x72, 0x65, 0x6e, 0x74, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x75, 0x65, 0x6c,
0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0d, 0x66, 0x75, 0x65, 0x6c, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12,
0x22, 0x0a, 0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73,
0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x64, 0x41,
0x74, 0x4d, 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x5f, 0x61, 0x74,
0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x6c, 0x61, 0x75, 0x6e, 0x63,
0x68, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x62, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61,
0x73, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e,
0x62, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2a,
0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e,
0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69, 0x62,
0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f,
0x70, 0x31, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0a, 0x74, 0x6f, 0x70, 0x31, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f,
0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x55, 0x73,
0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f,
0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
0x64, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x73, 0x68, 0x6f, 0x72,
0x74, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x6f, 0x6f, 0x6d,
0x53, 0x68, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x72, 0x6f, 0x63, 0x6b, 0x65,
0x74, 0x5f, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0e, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x76, 0x65, 0x72, 0x55, 0x72,
0x6c, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73,
0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73, 0x65, 0x74, 0x41, 0x74, 0x4d,
0x73, 0x22, 0xe6, 0x02, 0x0a, 0x12, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74,
0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b,
0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63,
0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02,
0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x6e,
0x65, 0x78, 0x74, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52,
0x09, 0x6e, 0x65, 0x78, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x24, 0x0a, 0x0e, 0x6c, 0x61,
0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0c, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73,
0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x67, 0x69,
0x66, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0d, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0c, 0x67, 0x69, 0x66, 0x74, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x64, 0x65,
0x12, 0x28, 0x0a, 0x10, 0x63, 0x70, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
0x74, 0x79, 0x70, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x70, 0x52, 0x65,
0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x69,
0x66, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x67,
0x69, 0x66, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x67, 0x69, 0x66, 0x74, 0x5f,
0x69, 0x63, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
0x67, 0x69, 0x66, 0x74, 0x49, 0x63, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x2c, 0x0a, 0x12, 0x67,
0x69, 0x66, 0x74, 0x5f, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72,
0x6c, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x66, 0x74, 0x41, 0x6e, 0x69,
0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x22, 0x4a, 0x0a, 0x0f, 0x52, 0x6f, 0x6f,
0x6d, 0x48, 0x65, 0x61, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05,
0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x64, 0x65, 0x6c,
0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x65,
0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e,
0x74, 0x48, 0x65, 0x61, 0x74, 0x22, 0x5f, 0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x61, 0x6e,
0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49,
0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f,
0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67, 0x69, 0x66,
0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x94, 0x02, 0x0a, 0x15, 0x52, 0x6f, 0x6f, 0x6d, 0x52,
0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e, 0x74,
0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x52, 0x6f, 0x6c,
0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x72, 0x65,
0x77, 0x61, 0x72, 0x64, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x64,
0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x67, 0x72, 0x6f,
0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c,
0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12,
0x19, 0x0a, 0x08, 0x69, 0x63, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28,
0x09, 0x52, 0x07, 0x69, 0x63, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72,
0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72,
0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18,
0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xeb, 0x03,
0x0a, 0x15, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x46, 0x75, 0x65, 0x6c,
0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65,
0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b,
0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20,
0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x64,
0x64, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
0x61, 0x64, 0x64, 0x65, 0x64, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x30, 0x0a, 0x14, 0x65, 0x66, 0x66,
0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x65,
0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69,
0x76, 0x65, 0x41, 0x64, 0x64, 0x65, 0x64, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x6f,
0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0c, 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x46, 0x75, 0x65, 0x6c,
0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x75, 0x65, 0x6c,
0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x46,
0x75, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x75, 0x65, 0x6c, 0x5f, 0x74, 0x68, 0x72, 0x65,
0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x66, 0x75, 0x65,
0x6c, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74,
0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74,
0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d,
0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73, 0x65, 0x74, 0x41, 0x74,
0x4d, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65,
0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64,
0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x69, 0x66, 0x74,
0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69, 0x66, 0x74, 0x49,
0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18,
0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74,
0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0d,
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12,
0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f,
0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69,
0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x82, 0x04, 0x0a, 0x11,
0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x67, 0x6e, 0x69, 0x74, 0x65,
0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14,
0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c,
0x65, 0x76, 0x65, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f,
0x66, 0x75, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72,
0x65, 0x6e, 0x74, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x75, 0x65, 0x6c, 0x5f,
0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0d, 0x66, 0x75, 0x65, 0x6c, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x22,
0x0a, 0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18,
0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x64, 0x41, 0x74,
0x4d, 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x5f, 0x61, 0x74, 0x5f,
0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68,
0x41, 0x74, 0x4d, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x62, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73,
0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x62,
0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2a, 0x0a,
0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f,
0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c,
0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x70,
0x31, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0a, 0x74, 0x6f, 0x70, 0x31, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x69,
0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65,
0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69,
0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x73, 0x68, 0x6f, 0x72, 0x74,
0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x6f, 0x6f, 0x6d, 0x53,
0x68, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74,
0x5f, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0e, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x76, 0x65, 0x72, 0x55, 0x72, 0x6c,
0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18,
0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73, 0x65, 0x74, 0x41, 0x74, 0x4d, 0x73,
0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x70, 0x31, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64,
0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x70, 0x31, 0x55, 0x73, 0x65, 0x72,
0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x67, 0x6e,
0x69, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x10, 0x69, 0x6e,
0x5f, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x08,
0x20, 0x03, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x6e, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72,
0x49, 0x64, 0x73, 0x12, 0x45, 0x0a, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x18, 0x09,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x65, 0x76, 0x65,
0x6e, 0x74, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6f, 0x6d,
0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e,
0x74, 0x52, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x93, 0x01, 0x0a, 0x17, 0x52,
0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x47,
0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74,
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b, 0x65,
0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01,
0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x45, 0x0a, 0x07, 0x72, 0x65, 0x77,
0x61, 0x72, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x79, 0x61,
0x70, 0x70, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e, 0x76,
0x31, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61,
0x72, 0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73,
0x42, 0x33, 0x5a, 0x31, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73,
0x2f, 0x72, 0x6f, 0x6f, 0x6d, 0x2f, 0x76, 0x31, 0x3b, 0x72, 0x6f, 0x6f, 0x6d, 0x65, 0x76, 0x65,
0x6e, 0x74, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73, 0x65, 0x74, 0x41, 0x74, 0x4d, 0x73,
0x22, 0xe6, 0x02, 0x0a, 0x12, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x4c,
0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65,
0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b,
0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20,
0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x65,
0x78, 0x74, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09,
0x6e, 0x65, 0x78, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x24, 0x0a, 0x0e, 0x6c, 0x61, 0x75,
0x6e, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28,
0x03, 0x52, 0x0c, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12,
0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05,
0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73, 0x65, 0x74, 0x41, 0x74, 0x4d, 0x73, 0x12,
0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x70, 0x31, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x70, 0x31, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65,
0x72, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x67, 0x6e, 0x69,
0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x10, 0x69, 0x6e, 0x5f,
0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x08, 0x20,
0x03, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x6e, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x73, 0x12, 0x45, 0x0a, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x18, 0x09, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x65, 0x76, 0x65, 0x6e,
0x74, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x52,
0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e, 0x74,
0x52, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x93, 0x01, 0x0a, 0x17, 0x52, 0x6f,
0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x47, 0x72,
0x61, 0x6e, 0x74, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74,
0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28,
0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x45, 0x0a, 0x07, 0x72, 0x65, 0x77, 0x61,
0x72, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e, 0x76, 0x31,
0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72,
0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x42,
0x33, 0x5a, 0x31, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61,
0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f,
0x72, 0x6f, 0x6f, 0x6d, 0x2f, 0x76, 0x31, 0x3b, 0x72, 0x6f, 0x6f, 0x6d, 0x65, 0x76, 0x65, 0x6e,
0x74, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

View File

@ -158,6 +158,11 @@ message RoomGiftSent {
int64 coin_spent = 10;
int64 country_id = 11;
int64 region_id = 12;
string gift_type_code = 13;
string cp_relation_type = 14;
string gift_name = 15;
string gift_icon_url = 16;
string gift_animation_url = 17;
}
// RoomHeatChanged

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc-gen-go-grpc v1.5.1
// - protoc v7.35.0
// source: proto/game/v1/game.proto
@ -76,10 +76,10 @@ type GameAppServiceServer interface {
type UnimplementedGameAppServiceServer struct{}
func (UnimplementedGameAppServiceServer) ListGames(context.Context, *ListGamesRequest) (*ListGamesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListGames not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListGames not implemented")
}
func (UnimplementedGameAppServiceServer) LaunchGame(context.Context, *LaunchGameRequest) (*LaunchGameResponse, error) {
return nil, status.Error(codes.Unimplemented, "method LaunchGame not implemented")
return nil, status.Errorf(codes.Unimplemented, "method LaunchGame not implemented")
}
func (UnimplementedGameAppServiceServer) mustEmbedUnimplementedGameAppServiceServer() {}
func (UnimplementedGameAppServiceServer) testEmbeddedByValue() {}
@ -92,7 +92,7 @@ type UnsafeGameAppServiceServer interface {
}
func RegisterGameAppServiceServer(s grpc.ServiceRegistrar, srv GameAppServiceServer) {
// If the following call panics, it indicates UnimplementedGameAppServiceServer was
// If the following call pancis, it indicates UnimplementedGameAppServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -203,7 +203,7 @@ type GameCallbackServiceServer interface {
type UnimplementedGameCallbackServiceServer struct{}
func (UnimplementedGameCallbackServiceServer) HandleCallback(context.Context, *CallbackRequest) (*CallbackResponse, error) {
return nil, status.Error(codes.Unimplemented, "method HandleCallback not implemented")
return nil, status.Errorf(codes.Unimplemented, "method HandleCallback not implemented")
}
func (UnimplementedGameCallbackServiceServer) mustEmbedUnimplementedGameCallbackServiceServer() {}
func (UnimplementedGameCallbackServiceServer) testEmbeddedByValue() {}
@ -216,7 +216,7 @@ type UnsafeGameCallbackServiceServer interface {
}
func RegisterGameCallbackServiceServer(s grpc.ServiceRegistrar, srv GameCallbackServiceServer) {
// If the following call panics, it indicates UnimplementedGameCallbackServiceServer was
// If the following call pancis, it indicates UnimplementedGameCallbackServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -309,7 +309,7 @@ type GameCronServiceServer interface {
type UnimplementedGameCronServiceServer struct{}
func (UnimplementedGameCronServiceServer) ProcessLevelEventOutboxBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessLevelEventOutboxBatch not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessLevelEventOutboxBatch not implemented")
}
func (UnimplementedGameCronServiceServer) mustEmbedUnimplementedGameCronServiceServer() {}
func (UnimplementedGameCronServiceServer) testEmbeddedByValue() {}
@ -322,7 +322,7 @@ type UnsafeGameCronServiceServer interface {
}
func RegisterGameCronServiceServer(s grpc.ServiceRegistrar, srv GameCronServiceServer) {
// If the following call panics, it indicates UnimplementedGameCronServiceServer was
// If the following call pancis, it indicates UnimplementedGameCronServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -476,22 +476,22 @@ type GameAdminServiceServer interface {
type UnimplementedGameAdminServiceServer struct{}
func (UnimplementedGameAdminServiceServer) ListPlatforms(context.Context, *ListPlatformsRequest) (*ListPlatformsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListPlatforms not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListPlatforms not implemented")
}
func (UnimplementedGameAdminServiceServer) UpsertPlatform(context.Context, *UpsertPlatformRequest) (*PlatformResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpsertPlatform not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpsertPlatform not implemented")
}
func (UnimplementedGameAdminServiceServer) ListCatalog(context.Context, *ListCatalogRequest) (*ListCatalogResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListCatalog not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListCatalog not implemented")
}
func (UnimplementedGameAdminServiceServer) UpsertCatalog(context.Context, *UpsertCatalogRequest) (*CatalogResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpsertCatalog not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpsertCatalog not implemented")
}
func (UnimplementedGameAdminServiceServer) SetGameStatus(context.Context, *SetGameStatusRequest) (*CatalogResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetGameStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetGameStatus not implemented")
}
func (UnimplementedGameAdminServiceServer) DeleteCatalog(context.Context, *DeleteCatalogRequest) (*DeleteCatalogResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteCatalog not implemented")
return nil, status.Errorf(codes.Unimplemented, "method DeleteCatalog not implemented")
}
func (UnimplementedGameAdminServiceServer) mustEmbedUnimplementedGameAdminServiceServer() {}
func (UnimplementedGameAdminServiceServer) testEmbeddedByValue() {}
@ -504,7 +504,7 @@ type UnsafeGameAdminServiceServer interface {
}
func RegisterGameAdminServiceServer(s grpc.ServiceRegistrar, srv GameAdminServiceServer) {
// If the following call panics, it indicates UnimplementedGameAdminServiceServer was
// If the following call pancis, it indicates UnimplementedGameAdminServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc-gen-go v1.35.1
// protoc v7.35.0
// source: proto/user/v1/auth.proto
@ -11,7 +11,6 @@ import (
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@ -23,12 +22,13 @@ const (
// LoginPasswordRequest 使用当前 display_user_id 和已设置密码登录。
type LoginPasswordRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
DisplayUserId string `protobuf:"bytes,2,opt,name=display_user_id,json=displayUserId,proto3" json:"display_user_id,omitempty"`
Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
DisplayUserId string `protobuf:"bytes,2,opt,name=display_user_id,json=displayUserId,proto3" json:"display_user_id,omitempty"`
Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
}
func (x *LoginPasswordRequest) Reset() {
@ -84,29 +84,30 @@ func (x *LoginPasswordRequest) GetPassword() string {
// LoginThirdPartyRequest 使用三方凭证登录或注册。
type LoginThirdPartyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"`
Credential string `protobuf:"bytes,3,opt,name=credential,proto3" json:"credential,omitempty"`
Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"`
Gender string `protobuf:"bytes,5,opt,name=gender,proto3" json:"gender,omitempty"`
Country string `protobuf:"bytes,6,opt,name=country,proto3" json:"country,omitempty"`
InviteCode string `protobuf:"bytes,7,opt,name=invite_code,json=inviteCode,proto3" json:"invite_code,omitempty"`
DeviceId string `protobuf:"bytes,8,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"`
Device string `protobuf:"bytes,9,opt,name=device,proto3" json:"device,omitempty"`
Avatar string `protobuf:"bytes,10,opt,name=avatar,proto3" json:"avatar,omitempty"`
Birth string `protobuf:"bytes,11,opt,name=birth,proto3" json:"birth,omitempty"`
AppVersion string `protobuf:"bytes,12,opt,name=app_version,json=appVersion,proto3" json:"app_version,omitempty"`
Source string `protobuf:"bytes,13,opt,name=source,proto3" json:"source,omitempty"`
Platform string `protobuf:"bytes,14,opt,name=platform,proto3" json:"platform,omitempty"`
Language string `protobuf:"bytes,15,opt,name=language,proto3" json:"language,omitempty"`
OsVersion string `protobuf:"bytes,16,opt,name=os_version,json=osVersion,proto3" json:"os_version,omitempty"`
BuildNumber string `protobuf:"bytes,17,opt,name=build_number,json=buildNumber,proto3" json:"build_number,omitempty"`
Timezone string `protobuf:"bytes,18,opt,name=timezone,proto3" json:"timezone,omitempty"`
InstallChannel string `protobuf:"bytes,19,opt,name=install_channel,json=installChannel,proto3" json:"install_channel,omitempty"`
Campaign string `protobuf:"bytes,20,opt,name=campaign,proto3" json:"campaign,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"`
Credential string `protobuf:"bytes,3,opt,name=credential,proto3" json:"credential,omitempty"`
Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"`
Gender string `protobuf:"bytes,5,opt,name=gender,proto3" json:"gender,omitempty"`
Country string `protobuf:"bytes,6,opt,name=country,proto3" json:"country,omitempty"`
InviteCode string `protobuf:"bytes,7,opt,name=invite_code,json=inviteCode,proto3" json:"invite_code,omitempty"`
DeviceId string `protobuf:"bytes,8,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"`
Device string `protobuf:"bytes,9,opt,name=device,proto3" json:"device,omitempty"`
Avatar string `protobuf:"bytes,10,opt,name=avatar,proto3" json:"avatar,omitempty"`
Birth string `protobuf:"bytes,11,opt,name=birth,proto3" json:"birth,omitempty"`
AppVersion string `protobuf:"bytes,12,opt,name=app_version,json=appVersion,proto3" json:"app_version,omitempty"`
Source string `protobuf:"bytes,13,opt,name=source,proto3" json:"source,omitempty"`
Platform string `protobuf:"bytes,14,opt,name=platform,proto3" json:"platform,omitempty"`
Language string `protobuf:"bytes,15,opt,name=language,proto3" json:"language,omitempty"`
OsVersion string `protobuf:"bytes,16,opt,name=os_version,json=osVersion,proto3" json:"os_version,omitempty"`
BuildNumber string `protobuf:"bytes,17,opt,name=build_number,json=buildNumber,proto3" json:"build_number,omitempty"`
Timezone string `protobuf:"bytes,18,opt,name=timezone,proto3" json:"timezone,omitempty"`
InstallChannel string `protobuf:"bytes,19,opt,name=install_channel,json=installChannel,proto3" json:"install_channel,omitempty"`
Campaign string `protobuf:"bytes,20,opt,name=campaign,proto3" json:"campaign,omitempty"`
}
func (x *LoginThirdPartyRequest) Reset() {
@ -281,13 +282,14 @@ func (x *LoginThirdPartyRequest) GetCampaign() string {
// AuthResponse 返回标准认证令牌。
type AuthResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Token *AuthToken `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
IsNewUser bool `protobuf:"varint,2,opt,name=is_new_user,json=isNewUser,proto3" json:"is_new_user,omitempty"`
ProfileCompleted bool `protobuf:"varint,3,opt,name=profile_completed,json=profileCompleted,proto3" json:"profile_completed,omitempty"`
OnboardingStatus string `protobuf:"bytes,4,opt,name=onboarding_status,json=onboardingStatus,proto3" json:"onboarding_status,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Token *AuthToken `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
IsNewUser bool `protobuf:"varint,2,opt,name=is_new_user,json=isNewUser,proto3" json:"is_new_user,omitempty"`
ProfileCompleted bool `protobuf:"varint,3,opt,name=profile_completed,json=profileCompleted,proto3" json:"profile_completed,omitempty"`
OnboardingStatus string `protobuf:"bytes,4,opt,name=onboarding_status,json=onboardingStatus,proto3" json:"onboarding_status,omitempty"`
}
func (x *AuthResponse) Reset() {
@ -350,12 +352,13 @@ func (x *AuthResponse) GetOnboardingStatus() string {
// SetPasswordRequest 为已经三方登录的用户首次设置密码。
type SetPasswordRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
}
func (x *SetPasswordRequest) Reset() {
@ -411,10 +414,11 @@ func (x *SetPasswordRequest) GetPassword() string {
// SetPasswordResponse 返回密码是否已经成功设置。
type SetPasswordResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
PasswordSet bool `protobuf:"varint,1,opt,name=password_set,json=passwordSet,proto3" json:"password_set,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
PasswordSet bool `protobuf:"varint,1,opt,name=password_set,json=passwordSet,proto3" json:"password_set,omitempty"`
}
func (x *SetPasswordResponse) Reset() {
@ -456,28 +460,29 @@ func (x *SetPasswordResponse) GetPasswordSet() bool {
// QuickCreateAccountRequest 为测试和三方联调快速创建一个可密码登录的完整账号。
type QuickCreateAccountRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
Avatar string `protobuf:"bytes,4,opt,name=avatar,proto3" json:"avatar,omitempty"`
Gender string `protobuf:"bytes,5,opt,name=gender,proto3" json:"gender,omitempty"`
Country string `protobuf:"bytes,6,opt,name=country,proto3" json:"country,omitempty"`
InviteCode string `protobuf:"bytes,7,opt,name=invite_code,json=inviteCode,proto3" json:"invite_code,omitempty"`
DeviceId string `protobuf:"bytes,8,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"`
Device string `protobuf:"bytes,9,opt,name=device,proto3" json:"device,omitempty"`
OsVersion string `protobuf:"bytes,10,opt,name=os_version,json=osVersion,proto3" json:"os_version,omitempty"`
Birth string `protobuf:"bytes,11,opt,name=birth,proto3" json:"birth,omitempty"`
AppVersion string `protobuf:"bytes,12,opt,name=app_version,json=appVersion,proto3" json:"app_version,omitempty"`
BuildNumber string `protobuf:"bytes,13,opt,name=build_number,json=buildNumber,proto3" json:"build_number,omitempty"`
Source string `protobuf:"bytes,14,opt,name=source,proto3" json:"source,omitempty"`
InstallChannel string `protobuf:"bytes,15,opt,name=install_channel,json=installChannel,proto3" json:"install_channel,omitempty"`
Campaign string `protobuf:"bytes,16,opt,name=campaign,proto3" json:"campaign,omitempty"`
Platform string `protobuf:"bytes,17,opt,name=platform,proto3" json:"platform,omitempty"`
Language string `protobuf:"bytes,18,opt,name=language,proto3" json:"language,omitempty"`
Timezone string `protobuf:"bytes,19,opt,name=timezone,proto3" json:"timezone,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
Avatar string `protobuf:"bytes,4,opt,name=avatar,proto3" json:"avatar,omitempty"`
Gender string `protobuf:"bytes,5,opt,name=gender,proto3" json:"gender,omitempty"`
Country string `protobuf:"bytes,6,opt,name=country,proto3" json:"country,omitempty"`
InviteCode string `protobuf:"bytes,7,opt,name=invite_code,json=inviteCode,proto3" json:"invite_code,omitempty"`
DeviceId string `protobuf:"bytes,8,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"`
Device string `protobuf:"bytes,9,opt,name=device,proto3" json:"device,omitempty"`
OsVersion string `protobuf:"bytes,10,opt,name=os_version,json=osVersion,proto3" json:"os_version,omitempty"`
Birth string `protobuf:"bytes,11,opt,name=birth,proto3" json:"birth,omitempty"`
AppVersion string `protobuf:"bytes,12,opt,name=app_version,json=appVersion,proto3" json:"app_version,omitempty"`
BuildNumber string `protobuf:"bytes,13,opt,name=build_number,json=buildNumber,proto3" json:"build_number,omitempty"`
Source string `protobuf:"bytes,14,opt,name=source,proto3" json:"source,omitempty"`
InstallChannel string `protobuf:"bytes,15,opt,name=install_channel,json=installChannel,proto3" json:"install_channel,omitempty"`
Campaign string `protobuf:"bytes,16,opt,name=campaign,proto3" json:"campaign,omitempty"`
Platform string `protobuf:"bytes,17,opt,name=platform,proto3" json:"platform,omitempty"`
Language string `protobuf:"bytes,18,opt,name=language,proto3" json:"language,omitempty"`
Timezone string `protobuf:"bytes,19,opt,name=timezone,proto3" json:"timezone,omitempty"`
}
func (x *QuickCreateAccountRequest) Reset() {
@ -645,11 +650,12 @@ func (x *QuickCreateAccountRequest) GetTimezone() string {
// QuickCreateAccountResponse 返回新账号登录态display_user_id 即后续账号密码登录用账号。
type QuickCreateAccountResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Token *AuthToken `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
PasswordSet bool `protobuf:"varint,2,opt,name=password_set,json=passwordSet,proto3" json:"password_set,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Token *AuthToken `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
PasswordSet bool `protobuf:"varint,2,opt,name=password_set,json=passwordSet,proto3" json:"password_set,omitempty"`
}
func (x *QuickCreateAccountResponse) Reset() {
@ -698,11 +704,12 @@ func (x *QuickCreateAccountResponse) GetPasswordSet() bool {
// RefreshTokenRequest 使用 refresh token 换取新 token。
type RefreshTokenRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
RefreshToken string `protobuf:"bytes,2,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
RefreshToken string `protobuf:"bytes,2,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
}
func (x *RefreshTokenRequest) Reset() {
@ -751,10 +758,11 @@ func (x *RefreshTokenRequest) GetRefreshToken() string {
// RefreshTokenResponse 返回轮换后的令牌。
type RefreshTokenResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Token *AuthToken `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Token *AuthToken `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
}
func (x *RefreshTokenResponse) Reset() {
@ -796,12 +804,13 @@ func (x *RefreshTokenResponse) GetToken() *AuthToken {
// LogoutRequest 失效指定会话或 refresh token。
type LogoutRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
RefreshToken string `protobuf:"bytes,3,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
RefreshToken string `protobuf:"bytes,3,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
}
func (x *LogoutRequest) Reset() {
@ -857,10 +866,11 @@ func (x *LogoutRequest) GetRefreshToken() string {
// LogoutResponse 返回会话是否被失效。
type LogoutResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Revoked bool `protobuf:"varint,1,opt,name=revoked,proto3" json:"revoked,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Revoked bool `protobuf:"varint,1,opt,name=revoked,proto3" json:"revoked,omitempty"`
}
func (x *LogoutResponse) Reset() {
@ -902,15 +912,16 @@ func (x *LogoutResponse) GetRevoked() bool {
// RecordLoginBlockedRequest 记录 gateway 入口快拦拒绝的登录审计。
type RecordLoginBlockedRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
Channel string `protobuf:"bytes,2,opt,name=channel,proto3" json:"channel,omitempty"`
Platform string `protobuf:"bytes,3,opt,name=platform,proto3" json:"platform,omitempty"`
Language string `protobuf:"bytes,4,opt,name=language,proto3" json:"language,omitempty"`
Timezone string `protobuf:"bytes,5,opt,name=timezone,proto3" json:"timezone,omitempty"`
BlockReason string `protobuf:"bytes,6,opt,name=block_reason,json=blockReason,proto3" json:"block_reason,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
Channel string `protobuf:"bytes,2,opt,name=channel,proto3" json:"channel,omitempty"`
Platform string `protobuf:"bytes,3,opt,name=platform,proto3" json:"platform,omitempty"`
Language string `protobuf:"bytes,4,opt,name=language,proto3" json:"language,omitempty"`
Timezone string `protobuf:"bytes,5,opt,name=timezone,proto3" json:"timezone,omitempty"`
BlockReason string `protobuf:"bytes,6,opt,name=block_reason,json=blockReason,proto3" json:"block_reason,omitempty"`
}
func (x *RecordLoginBlockedRequest) Reset() {
@ -987,10 +998,11 @@ func (x *RecordLoginBlockedRequest) GetBlockReason() string {
// RecordLoginBlockedResponse 返回审计写入是否成功。
type RecordLoginBlockedResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Recorded bool `protobuf:"varint,1,opt,name=recorded,proto3" json:"recorded,omitempty"`
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Recorded bool `protobuf:"varint,1,opt,name=recorded,proto3" json:"recorded,omitempty"`
}
func (x *RecordLoginBlockedResponse) Reset() {
@ -1030,123 +1042,372 @@ func (x *RecordLoginBlockedResponse) GetRecorded() bool {
return false
}
// AppHeartbeatRequest 刷新当前登录会话的 App 在线心跳。
type AppHeartbeatRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
SessionId string `protobuf:"bytes,3,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
}
func (x *AppHeartbeatRequest) Reset() {
*x = AppHeartbeatRequest{}
mi := &file_proto_user_v1_auth_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AppHeartbeatRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AppHeartbeatRequest) ProtoMessage() {}
func (x *AppHeartbeatRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[13]
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 AppHeartbeatRequest.ProtoReflect.Descriptor instead.
func (*AppHeartbeatRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{13}
}
func (x *AppHeartbeatRequest) GetMeta() *RequestMeta {
if x != nil {
return x.Meta
}
return nil
}
func (x *AppHeartbeatRequest) GetUserId() int64 {
if x != nil {
return x.UserId
}
return 0
}
func (x *AppHeartbeatRequest) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
// AppHeartbeatResponse 返回本次会话心跳写入时间。
type AppHeartbeatResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"`
HeartbeatAtMs int64 `protobuf:"varint,2,opt,name=heartbeat_at_ms,json=heartbeatAtMs,proto3" json:"heartbeat_at_ms,omitempty"`
ServerTimeMs int64 `protobuf:"varint,3,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
}
func (x *AppHeartbeatResponse) Reset() {
*x = AppHeartbeatResponse{}
mi := &file_proto_user_v1_auth_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AppHeartbeatResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AppHeartbeatResponse) ProtoMessage() {}
func (x *AppHeartbeatResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[14]
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 AppHeartbeatResponse.ProtoReflect.Descriptor instead.
func (*AppHeartbeatResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{14}
}
func (x *AppHeartbeatResponse) GetAccepted() bool {
if x != nil {
return x.Accepted
}
return false
}
func (x *AppHeartbeatResponse) GetHeartbeatAtMs() int64 {
if x != nil {
return x.HeartbeatAtMs
}
return 0
}
func (x *AppHeartbeatResponse) GetServerTimeMs() int64 {
if x != nil {
return x.ServerTimeMs
}
return 0
}
var File_proto_user_v1_auth_proto protoreflect.FileDescriptor
const file_proto_user_v1_auth_proto_rawDesc = "" +
"\n" +
"\x18proto/user/v1/auth.proto\x12\rhyapp.user.v1\x1a\x18proto/user/v1/user.proto\"\x8a\x01\n" +
"\x14LoginPasswordRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12&\n" +
"\x0fdisplay_user_id\x18\x02 \x01(\tR\rdisplayUserId\x12\x1a\n" +
"\bpassword\x18\x03 \x01(\tR\bpassword\"\xea\x04\n" +
"\x16LoginThirdPartyRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x1a\n" +
"\bprovider\x18\x02 \x01(\tR\bprovider\x12\x1e\n" +
"\n" +
"credential\x18\x03 \x01(\tR\n" +
"credential\x12\x1a\n" +
"\busername\x18\x04 \x01(\tR\busername\x12\x16\n" +
"\x06gender\x18\x05 \x01(\tR\x06gender\x12\x18\n" +
"\acountry\x18\x06 \x01(\tR\acountry\x12\x1f\n" +
"\vinvite_code\x18\a \x01(\tR\n" +
"inviteCode\x12\x1b\n" +
"\tdevice_id\x18\b \x01(\tR\bdeviceId\x12\x16\n" +
"\x06device\x18\t \x01(\tR\x06device\x12\x16\n" +
"\x06avatar\x18\n" +
" \x01(\tR\x06avatar\x12\x14\n" +
"\x05birth\x18\v \x01(\tR\x05birth\x12\x1f\n" +
"\vapp_version\x18\f \x01(\tR\n" +
"appVersion\x12\x16\n" +
"\x06source\x18\r \x01(\tR\x06source\x12\x1a\n" +
"\bplatform\x18\x0e \x01(\tR\bplatform\x12\x1a\n" +
"\blanguage\x18\x0f \x01(\tR\blanguage\x12\x1d\n" +
"\n" +
"os_version\x18\x10 \x01(\tR\tosVersion\x12!\n" +
"\fbuild_number\x18\x11 \x01(\tR\vbuildNumber\x12\x1a\n" +
"\btimezone\x18\x12 \x01(\tR\btimezone\x12'\n" +
"\x0finstall_channel\x18\x13 \x01(\tR\x0einstallChannel\x12\x1a\n" +
"\bcampaign\x18\x14 \x01(\tR\bcampaign\"\xb8\x01\n" +
"\fAuthResponse\x12.\n" +
"\x05token\x18\x01 \x01(\v2\x18.hyapp.user.v1.AuthTokenR\x05token\x12\x1e\n" +
"\vis_new_user\x18\x02 \x01(\bR\tisNewUser\x12+\n" +
"\x11profile_completed\x18\x03 \x01(\bR\x10profileCompleted\x12+\n" +
"\x11onboarding_status\x18\x04 \x01(\tR\x10onboardingStatus\"y\n" +
"\x12SetPasswordRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x17\n" +
"\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x1a\n" +
"\bpassword\x18\x03 \x01(\tR\bpassword\"8\n" +
"\x13SetPasswordResponse\x12!\n" +
"\fpassword_set\x18\x01 \x01(\bR\vpasswordSet\"\xcd\x04\n" +
"\x19QuickCreateAccountRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x1a\n" +
"\bpassword\x18\x02 \x01(\tR\bpassword\x12\x1a\n" +
"\busername\x18\x03 \x01(\tR\busername\x12\x16\n" +
"\x06avatar\x18\x04 \x01(\tR\x06avatar\x12\x16\n" +
"\x06gender\x18\x05 \x01(\tR\x06gender\x12\x18\n" +
"\acountry\x18\x06 \x01(\tR\acountry\x12\x1f\n" +
"\vinvite_code\x18\a \x01(\tR\n" +
"inviteCode\x12\x1b\n" +
"\tdevice_id\x18\b \x01(\tR\bdeviceId\x12\x16\n" +
"\x06device\x18\t \x01(\tR\x06device\x12\x1d\n" +
"\n" +
"os_version\x18\n" +
" \x01(\tR\tosVersion\x12\x14\n" +
"\x05birth\x18\v \x01(\tR\x05birth\x12\x1f\n" +
"\vapp_version\x18\f \x01(\tR\n" +
"appVersion\x12!\n" +
"\fbuild_number\x18\r \x01(\tR\vbuildNumber\x12\x16\n" +
"\x06source\x18\x0e \x01(\tR\x06source\x12'\n" +
"\x0finstall_channel\x18\x0f \x01(\tR\x0einstallChannel\x12\x1a\n" +
"\bcampaign\x18\x10 \x01(\tR\bcampaign\x12\x1a\n" +
"\bplatform\x18\x11 \x01(\tR\bplatform\x12\x1a\n" +
"\blanguage\x18\x12 \x01(\tR\blanguage\x12\x1a\n" +
"\btimezone\x18\x13 \x01(\tR\btimezone\"o\n" +
"\x1aQuickCreateAccountResponse\x12.\n" +
"\x05token\x18\x01 \x01(\v2\x18.hyapp.user.v1.AuthTokenR\x05token\x12!\n" +
"\fpassword_set\x18\x02 \x01(\bR\vpasswordSet\"j\n" +
"\x13RefreshTokenRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12#\n" +
"\rrefresh_token\x18\x02 \x01(\tR\frefreshToken\"F\n" +
"\x14RefreshTokenResponse\x12.\n" +
"\x05token\x18\x01 \x01(\v2\x18.hyapp.user.v1.AuthTokenR\x05token\"\x83\x01\n" +
"\rLogoutRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x1d\n" +
"\n" +
"session_id\x18\x02 \x01(\tR\tsessionId\x12#\n" +
"\rrefresh_token\x18\x03 \x01(\tR\frefreshToken\"*\n" +
"\x0eLogoutResponse\x12\x18\n" +
"\arevoked\x18\x01 \x01(\bR\arevoked\"\xdc\x01\n" +
"\x19RecordLoginBlockedRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x18\n" +
"\achannel\x18\x02 \x01(\tR\achannel\x12\x1a\n" +
"\bplatform\x18\x03 \x01(\tR\bplatform\x12\x1a\n" +
"\blanguage\x18\x04 \x01(\tR\blanguage\x12\x1a\n" +
"\btimezone\x18\x05 \x01(\tR\btimezone\x12!\n" +
"\fblock_reason\x18\x06 \x01(\tR\vblockReason\"8\n" +
"\x1aRecordLoginBlockedResponse\x12\x1a\n" +
"\brecorded\x18\x01 \x01(\bR\brecorded2\x83\x05\n" +
"\vAuthService\x12Q\n" +
"\rLoginPassword\x12#.hyapp.user.v1.LoginPasswordRequest\x1a\x1b.hyapp.user.v1.AuthResponse\x12U\n" +
"\x0fLoginThirdParty\x12%.hyapp.user.v1.LoginThirdPartyRequest\x1a\x1b.hyapp.user.v1.AuthResponse\x12T\n" +
"\vSetPassword\x12!.hyapp.user.v1.SetPasswordRequest\x1a\".hyapp.user.v1.SetPasswordResponse\x12i\n" +
"\x12QuickCreateAccount\x12(.hyapp.user.v1.QuickCreateAccountRequest\x1a).hyapp.user.v1.QuickCreateAccountResponse\x12W\n" +
"\fRefreshToken\x12\".hyapp.user.v1.RefreshTokenRequest\x1a#.hyapp.user.v1.RefreshTokenResponse\x12E\n" +
"\x06Logout\x12\x1c.hyapp.user.v1.LogoutRequest\x1a\x1d.hyapp.user.v1.LogoutResponse\x12i\n" +
"\x12RecordLoginBlocked\x12(.hyapp.user.v1.RecordLoginBlockedRequest\x1a).hyapp.user.v1.RecordLoginBlockedResponseB&Z$hyapp.local/api/proto/user/v1;userv1b\x06proto3"
var file_proto_user_v1_auth_proto_rawDesc = []byte{
0x0a, 0x18, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f,
0x61, 0x75, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x1a, 0x18, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x22, 0x8a, 0x01, 0x0a, 0x14, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04,
0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61,
0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x26, 0x0a, 0x0f,
0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73,
0x65, 0x72, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
0x22, 0xea, 0x04, 0x0a, 0x16, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50,
0x61, 0x72, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d,
0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x70,
0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70,
0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65,
0x6e, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x72, 0x65,
0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f,
0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f,
0x63, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x76, 0x69,
0x74, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65,
0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63,
0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x09, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61,
0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x76, 0x61,
0x74, 0x61, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x69, 0x72, 0x74, 0x68, 0x18, 0x0b, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x62, 0x69, 0x72, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x70, 0x70,
0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
0x61, 0x70, 0x70, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x0e,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1a,
0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x73,
0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x6f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x75, 0x69,
0x6c, 0x64, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08,
0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74,
0x61, 0x6c, 0x6c, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x13, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65,
0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x6d, 0x70, 0x61, 0x69, 0x67, 0x6e, 0x18, 0x14, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x6d, 0x70, 0x61, 0x69, 0x67, 0x6e, 0x22, 0xb8, 0x01,
0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e,
0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e,
0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75,
0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1e,
0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x6e, 0x65, 0x77, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20,
0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x4e, 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x12, 0x2b,
0x0a, 0x11, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65,
0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x66, 0x69,
0x6c, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x6f,
0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69,
0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x79, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x50,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e,
0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68,
0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17,
0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52,
0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77,
0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77,
0x6f, 0x72, 0x64, 0x22, 0x38, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
0x52, 0x0b, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x53, 0x65, 0x74, 0x22, 0xcd, 0x04,
0x0a, 0x19, 0x51, 0x75, 0x69, 0x63, 0x6b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d,
0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x70,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x04, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x67,
0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x65, 0x6e,
0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x06,
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1f, 0x0a,
0x0b, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1b,
0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64,
0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x76,
0x69, 0x63, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x69, 0x72, 0x74, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x62, 0x69, 0x72, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x70, 0x70, 0x5f,
0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61,
0x70, 0x70, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x75, 0x69,
0x6c, 0x64, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x5f,
0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69,
0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x1a, 0x0a,
0x08, 0x63, 0x61, 0x6d, 0x70, 0x61, 0x69, 0x67, 0x6e, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x63, 0x61, 0x6d, 0x70, 0x61, 0x69, 0x67, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61,
0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61,
0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67,
0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67,
0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x13, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x22, 0x6f, 0x0a,
0x1a, 0x51, 0x75, 0x69, 0x63, 0x6b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f,
0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x74,
0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x79, 0x61,
0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54,
0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x70,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x08, 0x52, 0x0b, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x53, 0x65, 0x74, 0x22, 0x6a,
0x0a, 0x13, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72,
0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52,
0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68,
0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65,
0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x46, 0x0a, 0x14, 0x52, 0x65,
0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x18, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76,
0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x05, 0x74, 0x6f, 0x6b,
0x65, 0x6e, 0x22, 0x83, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e,
0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04,
0x6d, 0x65, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f,
0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f,
0x6e, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74,
0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72,
0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x2a, 0x0a, 0x0e, 0x4c, 0x6f, 0x67, 0x6f,
0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65,
0x76, 0x6f, 0x6b, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x76,
0x6f, 0x6b, 0x65, 0x64, 0x22, 0xdc, 0x01, 0x0a, 0x19, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c,
0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65,
0x74, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08,
0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67,
0x75, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67,
0x75, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65,
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65,
0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e,
0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x61,
0x73, 0x6f, 0x6e, 0x22, 0x38, 0x0a, 0x1a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67,
0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65, 0x64, 0x22, 0x7d, 0x0a,
0x13, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e,
0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04,
0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a,
0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x80, 0x01, 0x0a,
0x14, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65,
0x64, 0x12, 0x26, 0x0a, 0x0f, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x5f, 0x61,
0x74, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x68, 0x65, 0x61, 0x72,
0x74, 0x62, 0x65, 0x61, 0x74, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72,
0x76, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
0x03, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x32,
0xdc, 0x05, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
0x51, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73,
0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x55, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64,
0x50, 0x61, 0x72, 0x74, 0x79, 0x12, 0x25, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73,
0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64,
0x50, 0x61, 0x72, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68,
0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74,
0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0b, 0x53, 0x65, 0x74,
0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70,
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73,
0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x79,
0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x69, 0x0a, 0x12, 0x51, 0x75, 0x69, 0x63, 0x6b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73,
0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x69, 0x63, 0x6b, 0x43, 0x72, 0x65, 0x61, 0x74,
0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e,
0x51, 0x75, 0x69, 0x63, 0x6b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x52, 0x65,
0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x2e, 0x68, 0x79, 0x61,
0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65,
0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23,
0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52,
0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e,
0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f,
0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x79,
0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f,
0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x12, 0x52, 0x65,
0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64,
0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63,
0x6b, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68, 0x79, 0x61,
0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72,
0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72,
0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73,
0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65,
0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61,
0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26,
0x5a, 0x24, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70,
0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b,
0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_proto_user_v1_auth_proto_rawDescOnce sync.Once
file_proto_user_v1_auth_proto_rawDescData []byte
file_proto_user_v1_auth_proto_rawDescData = file_proto_user_v1_auth_proto_rawDesc
)
func file_proto_user_v1_auth_proto_rawDescGZIP() []byte {
file_proto_user_v1_auth_proto_rawDescOnce.Do(func() {
file_proto_user_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_user_v1_auth_proto_rawDesc), len(file_proto_user_v1_auth_proto_rawDesc)))
file_proto_user_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_user_v1_auth_proto_rawDescData)
})
return file_proto_user_v1_auth_proto_rawDescData
}
var file_proto_user_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
var file_proto_user_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
var file_proto_user_v1_auth_proto_goTypes = []any{
(*LoginPasswordRequest)(nil), // 0: hyapp.user.v1.LoginPasswordRequest
(*LoginThirdPartyRequest)(nil), // 1: hyapp.user.v1.LoginThirdPartyRequest
@ -1161,39 +1422,44 @@ var file_proto_user_v1_auth_proto_goTypes = []any{
(*LogoutResponse)(nil), // 10: hyapp.user.v1.LogoutResponse
(*RecordLoginBlockedRequest)(nil), // 11: hyapp.user.v1.RecordLoginBlockedRequest
(*RecordLoginBlockedResponse)(nil), // 12: hyapp.user.v1.RecordLoginBlockedResponse
(*RequestMeta)(nil), // 13: hyapp.user.v1.RequestMeta
(*AuthToken)(nil), // 14: hyapp.user.v1.AuthToken
(*AppHeartbeatRequest)(nil), // 13: hyapp.user.v1.AppHeartbeatRequest
(*AppHeartbeatResponse)(nil), // 14: hyapp.user.v1.AppHeartbeatResponse
(*RequestMeta)(nil), // 15: hyapp.user.v1.RequestMeta
(*AuthToken)(nil), // 16: hyapp.user.v1.AuthToken
}
var file_proto_user_v1_auth_proto_depIdxs = []int32{
13, // 0: hyapp.user.v1.LoginPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta
13, // 1: hyapp.user.v1.LoginThirdPartyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
14, // 2: hyapp.user.v1.AuthResponse.token:type_name -> hyapp.user.v1.AuthToken
13, // 3: hyapp.user.v1.SetPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta
13, // 4: hyapp.user.v1.QuickCreateAccountRequest.meta:type_name -> hyapp.user.v1.RequestMeta
14, // 5: hyapp.user.v1.QuickCreateAccountResponse.token:type_name -> hyapp.user.v1.AuthToken
13, // 6: hyapp.user.v1.RefreshTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta
14, // 7: hyapp.user.v1.RefreshTokenResponse.token:type_name -> hyapp.user.v1.AuthToken
13, // 8: hyapp.user.v1.LogoutRequest.meta:type_name -> hyapp.user.v1.RequestMeta
13, // 9: hyapp.user.v1.RecordLoginBlockedRequest.meta:type_name -> hyapp.user.v1.RequestMeta
0, // 10: hyapp.user.v1.AuthService.LoginPassword:input_type -> hyapp.user.v1.LoginPasswordRequest
1, // 11: hyapp.user.v1.AuthService.LoginThirdParty:input_type -> hyapp.user.v1.LoginThirdPartyRequest
3, // 12: hyapp.user.v1.AuthService.SetPassword:input_type -> hyapp.user.v1.SetPasswordRequest
5, // 13: hyapp.user.v1.AuthService.QuickCreateAccount:input_type -> hyapp.user.v1.QuickCreateAccountRequest
7, // 14: hyapp.user.v1.AuthService.RefreshToken:input_type -> hyapp.user.v1.RefreshTokenRequest
9, // 15: hyapp.user.v1.AuthService.Logout:input_type -> hyapp.user.v1.LogoutRequest
11, // 16: hyapp.user.v1.AuthService.RecordLoginBlocked:input_type -> hyapp.user.v1.RecordLoginBlockedRequest
2, // 17: hyapp.user.v1.AuthService.LoginPassword:output_type -> hyapp.user.v1.AuthResponse
2, // 18: hyapp.user.v1.AuthService.LoginThirdParty:output_type -> hyapp.user.v1.AuthResponse
4, // 19: hyapp.user.v1.AuthService.SetPassword:output_type -> hyapp.user.v1.SetPasswordResponse
6, // 20: hyapp.user.v1.AuthService.QuickCreateAccount:output_type -> hyapp.user.v1.QuickCreateAccountResponse
8, // 21: hyapp.user.v1.AuthService.RefreshToken:output_type -> hyapp.user.v1.RefreshTokenResponse
10, // 22: hyapp.user.v1.AuthService.Logout:output_type -> hyapp.user.v1.LogoutResponse
12, // 23: hyapp.user.v1.AuthService.RecordLoginBlocked:output_type -> hyapp.user.v1.RecordLoginBlockedResponse
17, // [17:24] is the sub-list for method output_type
10, // [10:17] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension extendee
0, // [0:10] is the sub-list for field type_name
15, // 0: hyapp.user.v1.LoginPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta
15, // 1: hyapp.user.v1.LoginThirdPartyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
16, // 2: hyapp.user.v1.AuthResponse.token:type_name -> hyapp.user.v1.AuthToken
15, // 3: hyapp.user.v1.SetPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta
15, // 4: hyapp.user.v1.QuickCreateAccountRequest.meta:type_name -> hyapp.user.v1.RequestMeta
16, // 5: hyapp.user.v1.QuickCreateAccountResponse.token:type_name -> hyapp.user.v1.AuthToken
15, // 6: hyapp.user.v1.RefreshTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta
16, // 7: hyapp.user.v1.RefreshTokenResponse.token:type_name -> hyapp.user.v1.AuthToken
15, // 8: hyapp.user.v1.LogoutRequest.meta:type_name -> hyapp.user.v1.RequestMeta
15, // 9: hyapp.user.v1.RecordLoginBlockedRequest.meta:type_name -> hyapp.user.v1.RequestMeta
15, // 10: hyapp.user.v1.AppHeartbeatRequest.meta:type_name -> hyapp.user.v1.RequestMeta
0, // 11: hyapp.user.v1.AuthService.LoginPassword:input_type -> hyapp.user.v1.LoginPasswordRequest
1, // 12: hyapp.user.v1.AuthService.LoginThirdParty:input_type -> hyapp.user.v1.LoginThirdPartyRequest
3, // 13: hyapp.user.v1.AuthService.SetPassword:input_type -> hyapp.user.v1.SetPasswordRequest
5, // 14: hyapp.user.v1.AuthService.QuickCreateAccount:input_type -> hyapp.user.v1.QuickCreateAccountRequest
7, // 15: hyapp.user.v1.AuthService.RefreshToken:input_type -> hyapp.user.v1.RefreshTokenRequest
9, // 16: hyapp.user.v1.AuthService.Logout:input_type -> hyapp.user.v1.LogoutRequest
11, // 17: hyapp.user.v1.AuthService.RecordLoginBlocked:input_type -> hyapp.user.v1.RecordLoginBlockedRequest
13, // 18: hyapp.user.v1.AuthService.AppHeartbeat:input_type -> hyapp.user.v1.AppHeartbeatRequest
2, // 19: hyapp.user.v1.AuthService.LoginPassword:output_type -> hyapp.user.v1.AuthResponse
2, // 20: hyapp.user.v1.AuthService.LoginThirdParty:output_type -> hyapp.user.v1.AuthResponse
4, // 21: hyapp.user.v1.AuthService.SetPassword:output_type -> hyapp.user.v1.SetPasswordResponse
6, // 22: hyapp.user.v1.AuthService.QuickCreateAccount:output_type -> hyapp.user.v1.QuickCreateAccountResponse
8, // 23: hyapp.user.v1.AuthService.RefreshToken:output_type -> hyapp.user.v1.RefreshTokenResponse
10, // 24: hyapp.user.v1.AuthService.Logout:output_type -> hyapp.user.v1.LogoutResponse
12, // 25: hyapp.user.v1.AuthService.RecordLoginBlocked:output_type -> hyapp.user.v1.RecordLoginBlockedResponse
14, // 26: hyapp.user.v1.AuthService.AppHeartbeat:output_type -> hyapp.user.v1.AppHeartbeatResponse
19, // [19:27] is the sub-list for method output_type
11, // [11:19] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
}
func init() { file_proto_user_v1_auth_proto_init() }
@ -1206,9 +1472,9 @@ func file_proto_user_v1_auth_proto_init() {
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_user_v1_auth_proto_rawDesc), len(file_proto_user_v1_auth_proto_rawDesc)),
RawDescriptor: file_proto_user_v1_auth_proto_rawDesc,
NumEnums: 0,
NumMessages: 13,
NumMessages: 15,
NumExtensions: 0,
NumServices: 1,
},
@ -1217,6 +1483,7 @@ func file_proto_user_v1_auth_proto_init() {
MessageInfos: file_proto_user_v1_auth_proto_msgTypes,
}.Build()
File_proto_user_v1_auth_proto = out.File
file_proto_user_v1_auth_proto_rawDesc = nil
file_proto_user_v1_auth_proto_goTypes = nil
file_proto_user_v1_auth_proto_depIdxs = nil
}

View File

@ -124,6 +124,20 @@ message RecordLoginBlockedResponse {
bool recorded = 1;
}
// AppHeartbeatRequest App 线
message AppHeartbeatRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string session_id = 3;
}
// AppHeartbeatResponse
message AppHeartbeatResponse {
bool accepted = 1;
int64 heartbeat_at_ms = 2;
int64 server_time_ms = 3;
}
// AuthService token
service AuthService {
rpc LoginPassword(LoginPasswordRequest) returns (AuthResponse);
@ -133,4 +147,5 @@ service AuthService {
rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse);
rpc Logout(LogoutRequest) returns (LogoutResponse);
rpc RecordLoginBlocked(RecordLoginBlockedRequest) returns (RecordLoginBlockedResponse);
rpc AppHeartbeat(AppHeartbeatRequest) returns (AppHeartbeatResponse);
}

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc-gen-go-grpc v1.5.1
// - protoc v7.35.0
// source: proto/user/v1/auth.proto
@ -26,6 +26,7 @@ const (
AuthService_RefreshToken_FullMethodName = "/hyapp.user.v1.AuthService/RefreshToken"
AuthService_Logout_FullMethodName = "/hyapp.user.v1.AuthService/Logout"
AuthService_RecordLoginBlocked_FullMethodName = "/hyapp.user.v1.AuthService/RecordLoginBlocked"
AuthService_AppHeartbeat_FullMethodName = "/hyapp.user.v1.AuthService/AppHeartbeat"
)
// AuthServiceClient is the client API for AuthService service.
@ -41,6 +42,7 @@ type AuthServiceClient interface {
RefreshToken(ctx context.Context, in *RefreshTokenRequest, opts ...grpc.CallOption) (*RefreshTokenResponse, error)
Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error)
RecordLoginBlocked(ctx context.Context, in *RecordLoginBlockedRequest, opts ...grpc.CallOption) (*RecordLoginBlockedResponse, error)
AppHeartbeat(ctx context.Context, in *AppHeartbeatRequest, opts ...grpc.CallOption) (*AppHeartbeatResponse, error)
}
type authServiceClient struct {
@ -121,6 +123,16 @@ func (c *authServiceClient) RecordLoginBlocked(ctx context.Context, in *RecordLo
return out, nil
}
func (c *authServiceClient) AppHeartbeat(ctx context.Context, in *AppHeartbeatRequest, opts ...grpc.CallOption) (*AppHeartbeatResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AppHeartbeatResponse)
err := c.cc.Invoke(ctx, AuthService_AppHeartbeat_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AuthServiceServer is the server API for AuthService service.
// All implementations must embed UnimplementedAuthServiceServer
// for forward compatibility.
@ -134,6 +146,7 @@ type AuthServiceServer interface {
RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error)
Logout(context.Context, *LogoutRequest) (*LogoutResponse, error)
RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error)
AppHeartbeat(context.Context, *AppHeartbeatRequest) (*AppHeartbeatResponse, error)
mustEmbedUnimplementedAuthServiceServer()
}
@ -145,25 +158,28 @@ type AuthServiceServer interface {
type UnimplementedAuthServiceServer struct{}
func (UnimplementedAuthServiceServer) LoginPassword(context.Context, *LoginPasswordRequest) (*AuthResponse, error) {
return nil, status.Error(codes.Unimplemented, "method LoginPassword not implemented")
return nil, status.Errorf(codes.Unimplemented, "method LoginPassword not implemented")
}
func (UnimplementedAuthServiceServer) LoginThirdParty(context.Context, *LoginThirdPartyRequest) (*AuthResponse, error) {
return nil, status.Error(codes.Unimplemented, "method LoginThirdParty not implemented")
return nil, status.Errorf(codes.Unimplemented, "method LoginThirdParty not implemented")
}
func (UnimplementedAuthServiceServer) SetPassword(context.Context, *SetPasswordRequest) (*SetPasswordResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetPassword not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetPassword not implemented")
}
func (UnimplementedAuthServiceServer) QuickCreateAccount(context.Context, *QuickCreateAccountRequest) (*QuickCreateAccountResponse, error) {
return nil, status.Error(codes.Unimplemented, "method QuickCreateAccount not implemented")
return nil, status.Errorf(codes.Unimplemented, "method QuickCreateAccount not implemented")
}
func (UnimplementedAuthServiceServer) RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RefreshToken not implemented")
return nil, status.Errorf(codes.Unimplemented, "method RefreshToken not implemented")
}
func (UnimplementedAuthServiceServer) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Logout not implemented")
return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented")
}
func (UnimplementedAuthServiceServer) RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RecordLoginBlocked not implemented")
return nil, status.Errorf(codes.Unimplemented, "method RecordLoginBlocked not implemented")
}
func (UnimplementedAuthServiceServer) AppHeartbeat(context.Context, *AppHeartbeatRequest) (*AppHeartbeatResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AppHeartbeat not implemented")
}
func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}
func (UnimplementedAuthServiceServer) testEmbeddedByValue() {}
@ -176,7 +192,7 @@ type UnsafeAuthServiceServer interface {
}
func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) {
// If the following call panics, it indicates UnimplementedAuthServiceServer was
// If the following call pancis, it indicates UnimplementedAuthServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -312,6 +328,24 @@ func _AuthService_RecordLoginBlocked_Handler(srv interface{}, ctx context.Contex
return interceptor(ctx, in, info, handler)
}
func _AuthService_AppHeartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AppHeartbeatRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).AppHeartbeat(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_AppHeartbeat_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).AppHeartbeat(ctx, req.(*AppHeartbeatRequest))
}
return interceptor(ctx, in, info, handler)
}
// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -347,6 +381,10 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{
MethodName: "RecordLoginBlocked",
Handler: _AuthService_RecordLoginBlocked_Handler,
},
{
MethodName: "AppHeartbeat",
Handler: _AuthService_AppHeartbeat_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/user/v1/auth.proto",

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc-gen-go-grpc v1.5.1
// - protoc v7.35.0
// source: proto/user/v1/host.proto
@ -301,61 +301,61 @@ type UserHostServiceServer interface {
type UnimplementedUserHostServiceServer struct{}
func (UnimplementedUserHostServiceServer) SearchAgencies(context.Context, *SearchAgenciesRequest) (*SearchAgenciesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SearchAgencies not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SearchAgencies not implemented")
}
func (UnimplementedUserHostServiceServer) ApplyToAgency(context.Context, *ApplyToAgencyRequest) (*ApplyToAgencyResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ApplyToAgency not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ApplyToAgency not implemented")
}
func (UnimplementedUserHostServiceServer) ReviewAgencyApplication(context.Context, *ReviewAgencyApplicationRequest) (*ReviewAgencyApplicationResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ReviewAgencyApplication not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ReviewAgencyApplication not implemented")
}
func (UnimplementedUserHostServiceServer) KickAgencyHost(context.Context, *KickAgencyHostRequest) (*KickAgencyHostResponse, error) {
return nil, status.Error(codes.Unimplemented, "method KickAgencyHost not implemented")
return nil, status.Errorf(codes.Unimplemented, "method KickAgencyHost not implemented")
}
func (UnimplementedUserHostServiceServer) InviteAgency(context.Context, *InviteAgencyRequest) (*InviteAgencyResponse, error) {
return nil, status.Error(codes.Unimplemented, "method InviteAgency not implemented")
return nil, status.Errorf(codes.Unimplemented, "method InviteAgency not implemented")
}
func (UnimplementedUserHostServiceServer) InviteBD(context.Context, *InviteBDRequest) (*InviteBDResponse, error) {
return nil, status.Error(codes.Unimplemented, "method InviteBD not implemented")
return nil, status.Errorf(codes.Unimplemented, "method InviteBD not implemented")
}
func (UnimplementedUserHostServiceServer) ListBDLeaderBDs(context.Context, *ListBDLeaderBDsRequest) (*ListBDLeaderBDsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListBDLeaderBDs not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListBDLeaderBDs not implemented")
}
func (UnimplementedUserHostServiceServer) ListBDLeaderAgencies(context.Context, *ListBDLeaderAgenciesRequest) (*ListBDLeaderAgenciesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListBDLeaderAgencies not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListBDLeaderAgencies not implemented")
}
func (UnimplementedUserHostServiceServer) ListBDAgencies(context.Context, *ListBDAgenciesRequest) (*ListBDAgenciesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListBDAgencies not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListBDAgencies not implemented")
}
func (UnimplementedUserHostServiceServer) ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessRoleInvitation not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessRoleInvitation not implemented")
}
func (UnimplementedUserHostServiceServer) GetHostProfile(context.Context, *GetHostProfileRequest) (*GetHostProfileResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetHostProfile not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetHostProfile not implemented")
}
func (UnimplementedUserHostServiceServer) GetBDProfile(context.Context, *GetBDProfileRequest) (*GetBDProfileResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetBDProfile not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetBDProfile not implemented")
}
func (UnimplementedUserHostServiceServer) GetCoinSellerProfile(context.Context, *GetCoinSellerProfileRequest) (*GetCoinSellerProfileResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetCoinSellerProfile not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetCoinSellerProfile not implemented")
}
func (UnimplementedUserHostServiceServer) ListActiveCoinSellersInMyRegion(context.Context, *ListActiveCoinSellersInMyRegionRequest) (*ListActiveCoinSellersInMyRegionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListActiveCoinSellersInMyRegion not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListActiveCoinSellersInMyRegion not implemented")
}
func (UnimplementedUserHostServiceServer) GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetUserRoleSummary not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetUserRoleSummary not implemented")
}
func (UnimplementedUserHostServiceServer) GetAgency(context.Context, *GetAgencyRequest) (*GetAgencyResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetAgency not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetAgency not implemented")
}
func (UnimplementedUserHostServiceServer) CheckBusinessCapability(context.Context, *CheckBusinessCapabilityRequest) (*CheckBusinessCapabilityResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CheckBusinessCapability not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CheckBusinessCapability not implemented")
}
func (UnimplementedUserHostServiceServer) GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetAgencyMembers not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyMembers not implemented")
}
func (UnimplementedUserHostServiceServer) GetAgencyApplications(context.Context, *GetAgencyApplicationsRequest) (*GetAgencyApplicationsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetAgencyApplications not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyApplications not implemented")
}
func (UnimplementedUserHostServiceServer) mustEmbedUnimplementedUserHostServiceServer() {}
func (UnimplementedUserHostServiceServer) testEmbeddedByValue() {}
@ -368,7 +368,7 @@ type UnsafeUserHostServiceServer interface {
}
func RegisterUserHostServiceServer(s grpc.ServiceRegistrar, srv UserHostServiceServer) {
// If the following call panics, it indicates UnimplementedUserHostServiceServer was
// If the following call pancis, it indicates UnimplementedUserHostServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -961,31 +961,31 @@ type UserHostAdminServiceServer interface {
type UnimplementedUserHostAdminServiceServer struct{}
func (UnimplementedUserHostAdminServiceServer) CreateBDLeader(context.Context, *CreateBDLeaderRequest) (*CreateBDLeaderResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateBDLeader not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateBDLeader not implemented")
}
func (UnimplementedUserHostAdminServiceServer) CreateBD(context.Context, *CreateBDRequest) (*CreateBDResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateBD not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateBD not implemented")
}
func (UnimplementedUserHostAdminServiceServer) SetBDStatus(context.Context, *SetBDStatusRequest) (*SetBDStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetBDStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetBDStatus not implemented")
}
func (UnimplementedUserHostAdminServiceServer) CreateCoinSeller(context.Context, *CreateCoinSellerRequest) (*CreateCoinSellerResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateCoinSeller not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateCoinSeller not implemented")
}
func (UnimplementedUserHostAdminServiceServer) SetCoinSellerStatus(context.Context, *SetCoinSellerStatusRequest) (*SetCoinSellerStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetCoinSellerStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetCoinSellerStatus not implemented")
}
func (UnimplementedUserHostAdminServiceServer) CreateAgency(context.Context, *CreateAgencyRequest) (*CreateAgencyResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateAgency not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateAgency not implemented")
}
func (UnimplementedUserHostAdminServiceServer) CloseAgency(context.Context, *CloseAgencyRequest) (*CloseAgencyResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CloseAgency not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CloseAgency not implemented")
}
func (UnimplementedUserHostAdminServiceServer) DeleteAgency(context.Context, *DeleteAgencyRequest) (*DeleteAgencyResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteAgency not implemented")
return nil, status.Errorf(codes.Unimplemented, "method DeleteAgency not implemented")
}
func (UnimplementedUserHostAdminServiceServer) SetAgencyJoinEnabled(context.Context, *SetAgencyJoinEnabledRequest) (*SetAgencyJoinEnabledResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetAgencyJoinEnabled not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetAgencyJoinEnabled not implemented")
}
func (UnimplementedUserHostAdminServiceServer) mustEmbedUnimplementedUserHostAdminServiceServer() {}
func (UnimplementedUserHostAdminServiceServer) testEmbeddedByValue() {}
@ -998,7 +998,7 @@ type UnsafeUserHostAdminServiceServer interface {
}
func RegisterUserHostAdminServiceServer(s grpc.ServiceRegistrar, srv UserHostAdminServiceServer) {
// If the following call panics, it indicates UnimplementedUserHostAdminServiceServer was
// If the following call pancis, it indicates UnimplementedUserHostAdminServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.

File diff suppressed because it is too large Load Diff

View File

@ -360,6 +360,140 @@ message ListFriendApplicationsResponse {
int64 total = 2;
}
// CPUserProfile CP
message CPUserProfile {
int64 user_id = 1;
string display_user_id = 2;
string username = 3;
string avatar = 4;
}
// CPGiftSnapshot
message CPGiftSnapshot {
string gift_id = 1;
string gift_name = 2;
string gift_icon_url = 3;
string gift_animation_url = 4;
int32 gift_count = 5;
int64 gift_value = 6;
string billing_receipt_id = 7;
}
// CPApplication A B CP//
message CPApplication {
string application_id = 1;
string relation_type = 2;
string status = 3;
CPUserProfile requester = 4;
CPUserProfile target = 5;
string room_id = 6;
int64 room_region_id = 7;
CPGiftSnapshot gift = 8;
int64 created_at_ms = 9;
int64 updated_at_ms = 10;
int64 expires_at_ms = 11;
int64 decided_at_ms = 12;
}
// CPRelationship active
message CPRelationship {
string relationship_id = 1;
string relation_type = 2;
string status = 3;
CPUserProfile me = 4;
CPUserProfile partner = 5;
int64 intimacy_value = 6;
int32 level = 7;
reserved 8;
reserved "equipped";
int64 formed_at_ms = 9;
int64 updated_at_ms = 10;
}
message ListCPApplicationsRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string direction = 3;
string status = 4;
int32 page = 5;
int32 page_size = 6;
}
message ListCPApplicationsResponse {
repeated CPApplication applications = 1;
int64 total = 2;
int64 server_time_ms = 3;
}
message AcceptCPApplicationRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string application_id = 3;
}
message AcceptCPApplicationResponse {
CPApplication application = 1;
CPRelationship relationship = 2;
}
message RejectCPApplicationRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string application_id = 3;
string reason = 4;
}
message RejectCPApplicationResponse {
CPApplication application = 1;
}
message ListCPRelationshipsRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string relation_type = 3;
int32 page = 4;
int32 page_size = 5;
}
message ListCPRelationshipsResponse {
repeated CPRelationship relationships = 1;
int64 total = 2;
reserved 3;
reserved "max_count_by_relation_type";
}
message RoomGiftCPEvent {
RequestMeta meta = 1;
string event_id = 2;
string room_id = 3;
int64 room_version = 4;
int64 occurred_at_ms = 5;
int64 sender_user_id = 6;
int64 target_user_id = 7;
string gift_id = 8;
int32 gift_count = 9;
int64 gift_value = 10;
string billing_receipt_id = 11;
int64 visible_region_id = 12;
string command_id = 13;
string gift_type_code = 14;
string cp_relation_type = 15;
string gift_name = 16;
string gift_icon_url = 17;
string gift_animation_url = 18;
}
message ConsumeRoomGiftCPEventRequest {
RequestMeta meta = 1;
RoomGiftCPEvent event = 2;
}
message ConsumeRoomGiftCPEventResponse {
bool consumed = 1;
CPApplication application = 2;
CPRelationship relationship = 3;
}
// UserReport App
message UserReport {
string report_id = 1;
@ -758,6 +892,19 @@ service UserSocialService {
rpc SubmitReport(SubmitReportRequest) returns (SubmitReportResponse);
}
// UserCPService App CP//
service UserCPService {
rpc ListCPApplications(ListCPApplicationsRequest) returns (ListCPApplicationsResponse);
rpc AcceptCPApplication(AcceptCPApplicationRequest) returns (AcceptCPApplicationResponse);
rpc RejectCPApplication(RejectCPApplicationRequest) returns (RejectCPApplicationResponse);
rpc ListCPRelationships(ListCPRelationshipsRequest) returns (ListCPRelationshipsResponse);
}
// UserCPInternalService owner outbox worker App
service UserCPInternalService {
rpc ConsumeRoomGiftCPEvent(ConsumeRoomGiftCPEventRequest) returns (ConsumeRoomGiftCPEventResponse);
}
// UserCronService cron-service user-service owner
service UserCronService {
rpc ProcessLoginIPRiskBatch(CronBatchRequest) returns (CronBatchResponse);

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc-gen-go-grpc v1.5.1
// - protoc v7.35.0
// source: proto/user/v1/user.proto
@ -197,37 +197,37 @@ type UserServiceServer interface {
type UnimplementedUserServiceServer struct{}
func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetUser not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented")
}
func (UnimplementedUserServiceServer) BusinessUserLookup(context.Context, *BusinessUserLookupRequest) (*BusinessUserLookupResponse, error) {
return nil, status.Error(codes.Unimplemented, "method BusinessUserLookup not implemented")
return nil, status.Errorf(codes.Unimplemented, "method BusinessUserLookup not implemented")
}
func (UnimplementedUserServiceServer) GetMyProfileStats(context.Context, *GetMyProfileStatsRequest) (*GetMyProfileStatsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetMyProfileStats not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetMyProfileStats not implemented")
}
func (UnimplementedUserServiceServer) BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersResponse, error) {
return nil, status.Error(codes.Unimplemented, "method BatchGetUsers not implemented")
return nil, status.Errorf(codes.Unimplemented, "method BatchGetUsers not implemented")
}
func (UnimplementedUserServiceServer) ListUserIDs(context.Context, *ListUserIDsRequest) (*ListUserIDsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListUserIDs not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListUserIDs not implemented")
}
func (UnimplementedUserServiceServer) GetUserMicLifetimeStats(context.Context, *GetUserMicLifetimeStatsRequest) (*GetUserMicLifetimeStatsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetUserMicLifetimeStats not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetUserMicLifetimeStats not implemented")
}
func (UnimplementedUserServiceServer) UpdateUserProfile(context.Context, *UpdateUserProfileRequest) (*UpdateUserProfileResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateUserProfile not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateUserProfile not implemented")
}
func (UnimplementedUserServiceServer) ChangeUserCountry(context.Context, *ChangeUserCountryRequest) (*ChangeUserCountryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ChangeUserCountry not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ChangeUserCountry not implemented")
}
func (UnimplementedUserServiceServer) AdminChangeUserCountry(context.Context, *AdminChangeUserCountryRequest) (*ChangeUserCountryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminChangeUserCountry not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminChangeUserCountry not implemented")
}
func (UnimplementedUserServiceServer) SetUserStatus(context.Context, *SetUserStatusRequest) (*SetUserStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetUserStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetUserStatus not implemented")
}
func (UnimplementedUserServiceServer) CompleteOnboarding(context.Context, *CompleteOnboardingRequest) (*CompleteOnboardingResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CompleteOnboarding not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CompleteOnboarding not implemented")
}
func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {}
func (UnimplementedUserServiceServer) testEmbeddedByValue() {}
@ -240,7 +240,7 @@ type UnsafeUserServiceServer interface {
}
func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) {
// If the following call panics, it indicates UnimplementedUserServiceServer was
// If the following call pancis, it indicates UnimplementedUserServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -683,37 +683,37 @@ type UserSocialServiceServer interface {
type UnimplementedUserSocialServiceServer struct{}
func (UnimplementedUserSocialServiceServer) RecordProfileVisit(context.Context, *RecordProfileVisitRequest) (*RecordProfileVisitResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RecordProfileVisit not implemented")
return nil, status.Errorf(codes.Unimplemented, "method RecordProfileVisit not implemented")
}
func (UnimplementedUserSocialServiceServer) ListProfileVisitors(context.Context, *ListProfileVisitorsRequest) (*ListProfileVisitorsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListProfileVisitors not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListProfileVisitors not implemented")
}
func (UnimplementedUserSocialServiceServer) FollowUser(context.Context, *FollowUserRequest) (*FollowUserResponse, error) {
return nil, status.Error(codes.Unimplemented, "method FollowUser not implemented")
return nil, status.Errorf(codes.Unimplemented, "method FollowUser not implemented")
}
func (UnimplementedUserSocialServiceServer) UnfollowUser(context.Context, *UnfollowUserRequest) (*UnfollowUserResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UnfollowUser not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UnfollowUser not implemented")
}
func (UnimplementedUserSocialServiceServer) ListFollowing(context.Context, *ListFollowingRequest) (*ListFollowingResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListFollowing not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListFollowing not implemented")
}
func (UnimplementedUserSocialServiceServer) ApplyFriend(context.Context, *ApplyFriendRequest) (*ApplyFriendResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ApplyFriend not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ApplyFriend not implemented")
}
func (UnimplementedUserSocialServiceServer) AcceptFriendApplication(context.Context, *AcceptFriendApplicationRequest) (*AcceptFriendApplicationResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AcceptFriendApplication not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AcceptFriendApplication not implemented")
}
func (UnimplementedUserSocialServiceServer) DeleteFriend(context.Context, *DeleteFriendRequest) (*DeleteFriendResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteFriend not implemented")
return nil, status.Errorf(codes.Unimplemented, "method DeleteFriend not implemented")
}
func (UnimplementedUserSocialServiceServer) ListFriends(context.Context, *ListFriendsRequest) (*ListFriendsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListFriends not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListFriends not implemented")
}
func (UnimplementedUserSocialServiceServer) ListFriendApplications(context.Context, *ListFriendApplicationsRequest) (*ListFriendApplicationsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListFriendApplications not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListFriendApplications not implemented")
}
func (UnimplementedUserSocialServiceServer) SubmitReport(context.Context, *SubmitReportRequest) (*SubmitReportResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SubmitReport not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SubmitReport not implemented")
}
func (UnimplementedUserSocialServiceServer) mustEmbedUnimplementedUserSocialServiceServer() {}
func (UnimplementedUserSocialServiceServer) testEmbeddedByValue() {}
@ -726,7 +726,7 @@ type UnsafeUserSocialServiceServer interface {
}
func RegisterUserSocialServiceServer(s grpc.ServiceRegistrar, srv UserSocialServiceServer) {
// If the following call panics, it indicates UnimplementedUserSocialServiceServer was
// If the following call pancis, it indicates UnimplementedUserSocialServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -990,6 +990,332 @@ var UserSocialService_ServiceDesc = grpc.ServiceDesc{
Metadata: "proto/user/v1/user.proto",
}
const (
UserCPService_ListCPApplications_FullMethodName = "/hyapp.user.v1.UserCPService/ListCPApplications"
UserCPService_AcceptCPApplication_FullMethodName = "/hyapp.user.v1.UserCPService/AcceptCPApplication"
UserCPService_RejectCPApplication_FullMethodName = "/hyapp.user.v1.UserCPService/RejectCPApplication"
UserCPService_ListCPRelationships_FullMethodName = "/hyapp.user.v1.UserCPService/ListCPRelationships"
)
// UserCPServiceClient is the client API for UserCPService 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.
//
// UserCPService 承载 App 端 CP/兄弟/姐妹关系申请、处理和列表。
type UserCPServiceClient interface {
ListCPApplications(ctx context.Context, in *ListCPApplicationsRequest, opts ...grpc.CallOption) (*ListCPApplicationsResponse, error)
AcceptCPApplication(ctx context.Context, in *AcceptCPApplicationRequest, opts ...grpc.CallOption) (*AcceptCPApplicationResponse, error)
RejectCPApplication(ctx context.Context, in *RejectCPApplicationRequest, opts ...grpc.CallOption) (*RejectCPApplicationResponse, error)
ListCPRelationships(ctx context.Context, in *ListCPRelationshipsRequest, opts ...grpc.CallOption) (*ListCPRelationshipsResponse, error)
}
type userCPServiceClient struct {
cc grpc.ClientConnInterface
}
func NewUserCPServiceClient(cc grpc.ClientConnInterface) UserCPServiceClient {
return &userCPServiceClient{cc}
}
func (c *userCPServiceClient) ListCPApplications(ctx context.Context, in *ListCPApplicationsRequest, opts ...grpc.CallOption) (*ListCPApplicationsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListCPApplicationsResponse)
err := c.cc.Invoke(ctx, UserCPService_ListCPApplications_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userCPServiceClient) AcceptCPApplication(ctx context.Context, in *AcceptCPApplicationRequest, opts ...grpc.CallOption) (*AcceptCPApplicationResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AcceptCPApplicationResponse)
err := c.cc.Invoke(ctx, UserCPService_AcceptCPApplication_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userCPServiceClient) RejectCPApplication(ctx context.Context, in *RejectCPApplicationRequest, opts ...grpc.CallOption) (*RejectCPApplicationResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RejectCPApplicationResponse)
err := c.cc.Invoke(ctx, UserCPService_RejectCPApplication_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userCPServiceClient) ListCPRelationships(ctx context.Context, in *ListCPRelationshipsRequest, opts ...grpc.CallOption) (*ListCPRelationshipsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListCPRelationshipsResponse)
err := c.cc.Invoke(ctx, UserCPService_ListCPRelationships_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// UserCPServiceServer is the server API for UserCPService service.
// All implementations must embed UnimplementedUserCPServiceServer
// for forward compatibility.
//
// UserCPService 承载 App 端 CP/兄弟/姐妹关系申请、处理和列表。
type UserCPServiceServer interface {
ListCPApplications(context.Context, *ListCPApplicationsRequest) (*ListCPApplicationsResponse, error)
AcceptCPApplication(context.Context, *AcceptCPApplicationRequest) (*AcceptCPApplicationResponse, error)
RejectCPApplication(context.Context, *RejectCPApplicationRequest) (*RejectCPApplicationResponse, error)
ListCPRelationships(context.Context, *ListCPRelationshipsRequest) (*ListCPRelationshipsResponse, error)
mustEmbedUnimplementedUserCPServiceServer()
}
// UnimplementedUserCPServiceServer 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 UnimplementedUserCPServiceServer struct{}
func (UnimplementedUserCPServiceServer) ListCPApplications(context.Context, *ListCPApplicationsRequest) (*ListCPApplicationsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListCPApplications not implemented")
}
func (UnimplementedUserCPServiceServer) AcceptCPApplication(context.Context, *AcceptCPApplicationRequest) (*AcceptCPApplicationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AcceptCPApplication not implemented")
}
func (UnimplementedUserCPServiceServer) RejectCPApplication(context.Context, *RejectCPApplicationRequest) (*RejectCPApplicationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RejectCPApplication not implemented")
}
func (UnimplementedUserCPServiceServer) ListCPRelationships(context.Context, *ListCPRelationshipsRequest) (*ListCPRelationshipsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListCPRelationships not implemented")
}
func (UnimplementedUserCPServiceServer) mustEmbedUnimplementedUserCPServiceServer() {}
func (UnimplementedUserCPServiceServer) testEmbeddedByValue() {}
// UnsafeUserCPServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to UserCPServiceServer will
// result in compilation errors.
type UnsafeUserCPServiceServer interface {
mustEmbedUnimplementedUserCPServiceServer()
}
func RegisterUserCPServiceServer(s grpc.ServiceRegistrar, srv UserCPServiceServer) {
// If the following call pancis, it indicates UnimplementedUserCPServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&UserCPService_ServiceDesc, srv)
}
func _UserCPService_ListCPApplications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListCPApplicationsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserCPServiceServer).ListCPApplications(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserCPService_ListCPApplications_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserCPServiceServer).ListCPApplications(ctx, req.(*ListCPApplicationsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserCPService_AcceptCPApplication_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AcceptCPApplicationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserCPServiceServer).AcceptCPApplication(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserCPService_AcceptCPApplication_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserCPServiceServer).AcceptCPApplication(ctx, req.(*AcceptCPApplicationRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserCPService_RejectCPApplication_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RejectCPApplicationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserCPServiceServer).RejectCPApplication(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserCPService_RejectCPApplication_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserCPServiceServer).RejectCPApplication(ctx, req.(*RejectCPApplicationRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserCPService_ListCPRelationships_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListCPRelationshipsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserCPServiceServer).ListCPRelationships(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserCPService_ListCPRelationships_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserCPServiceServer).ListCPRelationships(ctx, req.(*ListCPRelationshipsRequest))
}
return interceptor(ctx, in, info, handler)
}
// UserCPService_ServiceDesc is the grpc.ServiceDesc for UserCPService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var UserCPService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "hyapp.user.v1.UserCPService",
HandlerType: (*UserCPServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ListCPApplications",
Handler: _UserCPService_ListCPApplications_Handler,
},
{
MethodName: "AcceptCPApplication",
Handler: _UserCPService_AcceptCPApplication_Handler,
},
{
MethodName: "RejectCPApplication",
Handler: _UserCPService_RejectCPApplication_Handler,
},
{
MethodName: "ListCPRelationships",
Handler: _UserCPService_ListCPRelationships_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/user/v1/user.proto",
}
const (
UserCPInternalService_ConsumeRoomGiftCPEvent_FullMethodName = "/hyapp.user.v1.UserCPInternalService/ConsumeRoomGiftCPEvent"
)
// UserCPInternalServiceClient is the client API for UserCPInternalService 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.
//
// UserCPInternalService 给 owner outbox 消费 worker 调用,不暴露给 App 或后台。
type UserCPInternalServiceClient interface {
ConsumeRoomGiftCPEvent(ctx context.Context, in *ConsumeRoomGiftCPEventRequest, opts ...grpc.CallOption) (*ConsumeRoomGiftCPEventResponse, error)
}
type userCPInternalServiceClient struct {
cc grpc.ClientConnInterface
}
func NewUserCPInternalServiceClient(cc grpc.ClientConnInterface) UserCPInternalServiceClient {
return &userCPInternalServiceClient{cc}
}
func (c *userCPInternalServiceClient) ConsumeRoomGiftCPEvent(ctx context.Context, in *ConsumeRoomGiftCPEventRequest, opts ...grpc.CallOption) (*ConsumeRoomGiftCPEventResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ConsumeRoomGiftCPEventResponse)
err := c.cc.Invoke(ctx, UserCPInternalService_ConsumeRoomGiftCPEvent_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.
//
// UserCPInternalService 给 owner outbox 消费 worker 调用,不暴露给 App 或后台。
type UserCPInternalServiceServer interface {
ConsumeRoomGiftCPEvent(context.Context, *ConsumeRoomGiftCPEventRequest) (*ConsumeRoomGiftCPEventResponse, error)
mustEmbedUnimplementedUserCPInternalServiceServer()
}
// UnimplementedUserCPInternalServiceServer 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 UnimplementedUserCPInternalServiceServer struct{}
func (UnimplementedUserCPInternalServiceServer) ConsumeRoomGiftCPEvent(context.Context, *ConsumeRoomGiftCPEventRequest) (*ConsumeRoomGiftCPEventResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ConsumeRoomGiftCPEvent not implemented")
}
func (UnimplementedUserCPInternalServiceServer) mustEmbedUnimplementedUserCPInternalServiceServer() {}
func (UnimplementedUserCPInternalServiceServer) testEmbeddedByValue() {}
// UnsafeUserCPInternalServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to UserCPInternalServiceServer will
// result in compilation errors.
type UnsafeUserCPInternalServiceServer interface {
mustEmbedUnimplementedUserCPInternalServiceServer()
}
func RegisterUserCPInternalServiceServer(s grpc.ServiceRegistrar, srv UserCPInternalServiceServer) {
// If the following call pancis, it indicates UnimplementedUserCPInternalServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&UserCPInternalService_ServiceDesc, srv)
}
func _UserCPInternalService_ConsumeRoomGiftCPEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ConsumeRoomGiftCPEventRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserCPInternalServiceServer).ConsumeRoomGiftCPEvent(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserCPInternalService_ConsumeRoomGiftCPEvent_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserCPInternalServiceServer).ConsumeRoomGiftCPEvent(ctx, req.(*ConsumeRoomGiftCPEventRequest))
}
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)
var UserCPInternalService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "hyapp.user.v1.UserCPInternalService",
HandlerType: (*UserCPInternalServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ConsumeRoomGiftCPEvent",
Handler: _UserCPInternalService_ConsumeRoomGiftCPEvent_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/user/v1/user.proto",
}
const (
UserCronService_ProcessLoginIPRiskBatch_FullMethodName = "/hyapp.user.v1.UserCronService/ProcessLoginIPRiskBatch"
UserCronService_ProcessRegionRebuildBatch_FullMethodName = "/hyapp.user.v1.UserCronService/ProcessRegionRebuildBatch"
@ -1065,13 +1391,13 @@ type UserCronServiceServer interface {
type UnimplementedUserCronServiceServer struct{}
func (UnimplementedUserCronServiceServer) ProcessLoginIPRiskBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessLoginIPRiskBatch not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessLoginIPRiskBatch not implemented")
}
func (UnimplementedUserCronServiceServer) ProcessRegionRebuildBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessRegionRebuildBatch not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessRegionRebuildBatch not implemented")
}
func (UnimplementedUserCronServiceServer) CompensateMicOpenSessions(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CompensateMicOpenSessions not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CompensateMicOpenSessions not implemented")
}
func (UnimplementedUserCronServiceServer) mustEmbedUnimplementedUserCronServiceServer() {}
func (UnimplementedUserCronServiceServer) testEmbeddedByValue() {}
@ -1084,7 +1410,7 @@ type UnsafeUserCronServiceServer interface {
}
func RegisterUserCronServiceServer(s grpc.ServiceRegistrar, srv UserCronServiceServer) {
// If the following call panics, it indicates UnimplementedUserCronServiceServer was
// If the following call pancis, it indicates UnimplementedUserCronServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -1234,10 +1560,10 @@ type UserDeviceServiceServer interface {
type UnimplementedUserDeviceServiceServer struct{}
func (UnimplementedUserDeviceServiceServer) BindPushToken(context.Context, *BindPushTokenRequest) (*BindPushTokenResponse, error) {
return nil, status.Error(codes.Unimplemented, "method BindPushToken not implemented")
return nil, status.Errorf(codes.Unimplemented, "method BindPushToken not implemented")
}
func (UnimplementedUserDeviceServiceServer) DeletePushToken(context.Context, *DeletePushTokenRequest) (*DeletePushTokenResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeletePushToken not implemented")
return nil, status.Errorf(codes.Unimplemented, "method DeletePushToken not implemented")
}
func (UnimplementedUserDeviceServiceServer) mustEmbedUnimplementedUserDeviceServiceServer() {}
func (UnimplementedUserDeviceServiceServer) testEmbeddedByValue() {}
@ -1250,7 +1576,7 @@ type UnsafeUserDeviceServiceServer interface {
}
func RegisterUserDeviceServiceServer(s grpc.ServiceRegistrar, srv UserDeviceServiceServer) {
// If the following call panics, it indicates UnimplementedUserDeviceServiceServer was
// If the following call pancis, it indicates UnimplementedUserDeviceServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -1365,7 +1691,7 @@ type AppRegistryServiceServer interface {
type UnimplementedAppRegistryServiceServer struct{}
func (UnimplementedAppRegistryServiceServer) ResolveApp(context.Context, *ResolveAppRequest) (*ResolveAppResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ResolveApp not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ResolveApp not implemented")
}
func (UnimplementedAppRegistryServiceServer) mustEmbedUnimplementedAppRegistryServiceServer() {}
func (UnimplementedAppRegistryServiceServer) testEmbeddedByValue() {}
@ -1378,7 +1704,7 @@ type UnsafeAppRegistryServiceServer interface {
}
func RegisterAppRegistryServiceServer(s grpc.ServiceRegistrar, srv AppRegistryServiceServer) {
// If the following call panics, it indicates UnimplementedAppRegistryServiceServer was
// If the following call pancis, it indicates UnimplementedAppRegistryServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -1486,10 +1812,10 @@ type CountryAdminServiceServer interface {
type UnimplementedCountryAdminServiceServer struct{}
func (UnimplementedCountryAdminServiceServer) ListCountries(context.Context, *ListCountriesRequest) (*ListCountriesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListCountries not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListCountries not implemented")
}
func (UnimplementedCountryAdminServiceServer) UpdateCountry(context.Context, *UpdateCountryRequest) (*CountryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateCountry not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateCountry not implemented")
}
func (UnimplementedCountryAdminServiceServer) mustEmbedUnimplementedCountryAdminServiceServer() {}
func (UnimplementedCountryAdminServiceServer) testEmbeddedByValue() {}
@ -1502,7 +1828,7 @@ type UnsafeCountryAdminServiceServer interface {
}
func RegisterCountryAdminServiceServer(s grpc.ServiceRegistrar, srv CountryAdminServiceServer) {
// If the following call panics, it indicates UnimplementedCountryAdminServiceServer was
// If the following call pancis, it indicates UnimplementedCountryAdminServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -1630,10 +1956,10 @@ type CountryQueryServiceServer interface {
type UnimplementedCountryQueryServiceServer struct{}
func (UnimplementedCountryQueryServiceServer) ListRegistrationCountries(context.Context, *ListRegistrationCountriesRequest) (*ListRegistrationCountriesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRegistrationCountries not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRegistrationCountries not implemented")
}
func (UnimplementedCountryQueryServiceServer) ListLoginRiskBlockedCountries(context.Context, *ListLoginRiskBlockedCountriesRequest) (*ListLoginRiskBlockedCountriesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListLoginRiskBlockedCountries not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListLoginRiskBlockedCountries not implemented")
}
func (UnimplementedCountryQueryServiceServer) mustEmbedUnimplementedCountryQueryServiceServer() {}
func (UnimplementedCountryQueryServiceServer) testEmbeddedByValue() {}
@ -1646,7 +1972,7 @@ type UnsafeCountryQueryServiceServer interface {
}
func RegisterCountryQueryServiceServer(s grpc.ServiceRegistrar, srv CountryQueryServiceServer) {
// If the following call panics, it indicates UnimplementedCountryQueryServiceServer was
// If the following call pancis, it indicates UnimplementedCountryQueryServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -1802,16 +2128,16 @@ type RegionAdminServiceServer interface {
type UnimplementedRegionAdminServiceServer struct{}
func (UnimplementedRegionAdminServiceServer) ListRegions(context.Context, *ListRegionsRequest) (*ListRegionsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRegions not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRegions not implemented")
}
func (UnimplementedRegionAdminServiceServer) GetRegion(context.Context, *GetRegionRequest) (*RegionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRegion not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetRegion not implemented")
}
func (UnimplementedRegionAdminServiceServer) UpdateRegion(context.Context, *UpdateRegionRequest) (*RegionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateRegion not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateRegion not implemented")
}
func (UnimplementedRegionAdminServiceServer) ReplaceRegionCountries(context.Context, *ReplaceRegionCountriesRequest) (*RegionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ReplaceRegionCountries not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ReplaceRegionCountries not implemented")
}
func (UnimplementedRegionAdminServiceServer) mustEmbedUnimplementedRegionAdminServiceServer() {}
func (UnimplementedRegionAdminServiceServer) testEmbeddedByValue() {}
@ -1824,7 +2150,7 @@ type UnsafeRegionAdminServiceServer interface {
}
func RegisterRegionAdminServiceServer(s grpc.ServiceRegistrar, srv RegionAdminServiceServer) {
// If the following call panics, it indicates UnimplementedRegionAdminServiceServer was
// If the following call pancis, it indicates UnimplementedRegionAdminServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -2035,19 +2361,19 @@ type UserIdentityServiceServer interface {
type UnimplementedUserIdentityServiceServer struct{}
func (UnimplementedUserIdentityServiceServer) GetUserIdentity(context.Context, *GetUserIdentityRequest) (*GetUserIdentityResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetUserIdentity not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetUserIdentity not implemented")
}
func (UnimplementedUserIdentityServiceServer) ResolveDisplayUserID(context.Context, *ResolveDisplayUserIDRequest) (*ResolveDisplayUserIDResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ResolveDisplayUserID not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ResolveDisplayUserID not implemented")
}
func (UnimplementedUserIdentityServiceServer) ChangeDisplayUserID(context.Context, *ChangeDisplayUserIDRequest) (*ChangeDisplayUserIDResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ChangeDisplayUserID not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ChangeDisplayUserID not implemented")
}
func (UnimplementedUserIdentityServiceServer) ApplyPrettyDisplayUserID(context.Context, *ApplyPrettyDisplayUserIDRequest) (*ApplyPrettyDisplayUserIDResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ApplyPrettyDisplayUserID not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ApplyPrettyDisplayUserID not implemented")
}
func (UnimplementedUserIdentityServiceServer) ExpirePrettyDisplayUserID(context.Context, *ExpirePrettyDisplayUserIDRequest) (*ExpirePrettyDisplayUserIDResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ExpirePrettyDisplayUserID not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ExpirePrettyDisplayUserID not implemented")
}
func (UnimplementedUserIdentityServiceServer) mustEmbedUnimplementedUserIdentityServiceServer() {}
func (UnimplementedUserIdentityServiceServer) testEmbeddedByValue() {}
@ -2060,7 +2386,7 @@ type UnsafeUserIdentityServiceServer interface {
}
func RegisterUserIdentityServiceServer(s grpc.ServiceRegistrar, srv UserIdentityServiceServer) {
// If the following call panics, it indicates UnimplementedUserIdentityServiceServer was
// If the following call pancis, it indicates UnimplementedUserIdentityServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,11 @@ message DebitGiftResponse {
int64 host_period_diamond_added = 11;
// host_period_cycle_key UTC 2026-05
string host_period_cycle_key = 12;
// cp_relation_type CP room outbox
string cp_relation_type = 13;
string gift_name = 14;
string gift_icon_url = 15;
string gift_animation_url = 16;
}
// DebitGiftTarget
@ -387,6 +392,7 @@ message GiftConfig {
int64 effective_from_ms = 20;
int64 effective_to_ms = 21;
repeated string effect_types = 22;
string cp_relation_type = 23;
}
message GiftTypeConfig {
@ -694,6 +700,7 @@ message CreateGiftConfigRequest {
int64 effective_from_ms = 18;
int64 effective_to_ms = 19;
repeated string effect_types = 20;
string cp_relation_type = 21;
}
message UpdateGiftConfigRequest {
@ -718,6 +725,7 @@ message UpdateGiftConfigRequest {
int64 effective_from_ms = 18;
int64 effective_to_ms = 19;
repeated string effect_types = 20;
string cp_relation_type = 21;
}
message SetGiftConfigStatusRequest {

View File

@ -0,0 +1,233 @@
# CP关系服务端开发文档
本文定义当前 CP、兄弟、姐妹关系的服务端边界。关系事实归 `user-service`,送礼扣费归 `wallet-service`,房间送礼事实归 `room-service`CP 相关 IM 由 `notice-service` 消费 `user_outbox` 后补偿投递。
## 1. 关系规则
| 类型 | 编码 | 说明 |
| --- | --- | --- |
| CP | `cp` | CP 关系 |
| 兄弟 | `brother` | 兄弟关系 |
| 姐妹 | `sister` | 姐妹关系 |
规则:
- 每个用户同时只能有一个 active 关系,不按类型拆数量,也不需要后台配置数量。
- A 和 B 同时只能存在一种 active 关系。
- A 可以给 B 发送 `cp``brother``sister` 多种 CP 礼物,形成多条 pending 申请。
- B 接受其中一个申请后,同一对用户其他 pending 申请会被置为 `blocked`,不能再接受。
- 申请默认 24 小时过期,当前不提供后台配置入口。
- 不提供佩戴 CP 功能;资料卡如果要展示关系,直接展示用户当前 active 关系。
## 2. 服务归属
| 服务 | 负责内容 |
| --- | --- |
| `wallet-service` | 礼物配置、扣费、热度值、礼物 icon 和动画快照 |
| `room-service` | 送礼命令、房间热度、房间礼物 IM、`RoomGiftSent` outbox |
| `user-service` | CP 申请、关系、亲密值、等级、等级奖励幂等、`user_outbox` |
| `gateway-service` | App HTTP 接口、鉴权、request_id 透传 |
| `notice-service` | 申请、同意、拒绝、关系成立的 C2C/房间/区域 IM |
| `cron-service` | 定时触发 user-service 过期申请处理 |
边界:
- `room-service` 不保存 CP 关系状态。
- `user-service` 不直连 `wallet-service``room-service``notice-service` 数据库。
- CP 申请和亲密值变化只能消费 `RoomGiftSent` MQ 事实,不能扫 `room_outbox`
- 下游服务只能消费 owner service 发布到 RocketMQ 的 outbox。
## 3. CP礼物配置
礼物大类使用 `gift_type_code=cp`。CP 子类型使用发送时快照字段 `cp_relation_type`,值为 `cp``brother``sister`
| 配置 | 说明 |
| --- | --- |
| 礼物类型 | `gift_type_code=cp`,礼物面板展示为 CP 礼物 Tab |
| CP 子类型 | `cp_relation_type=cp/brother/sister`,用于 CP Tab 二级分类 |
| 礼物 icon | 来自绑定资源的 `asset_url`,发送事件保存快照 |
| 礼物价格和热度 | 继续由 `wallet-service` 结算,客户端不能传价格或热度 |
`cp_relation_type` 写入礼物 `presentation_json`
```json
{
"cp_relation_type": "cp",
"cp_relation_label": "CP礼物"
}
```
## 4. RoomGiftSent字段
`api/proto/events/room/v1/events.proto``RoomGiftSent` 必须带以下送礼快照:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `gift_type_code` | string | CP 礼物为 `cp` |
| `cp_relation_type` | string | `cp``brother``sister`;非 CP 礼物为空 |
| `gift_name` | string | 礼物名称 |
| `gift_icon_url` | string | 礼物 icon |
| `gift_animation_url` | string | 礼物动画,可为空 |
## 5. 送礼触发申请
```mermaid
sequenceDiagram
participant F as Flutter
participant G as gateway-service
participant R as room-service
participant W as wallet-service
participant U as user-service
participant N as notice-service
participant IM as Tencent IM
F->>G: POST /api/v1/rooms/gift/send
G->>R: SendGift
R->>W: DebitGift
W-->>R: heat_value + gift_type_code + cp_relation_type + gift snapshot
R->>R: update heat/rank/command log
R-->>F: SendGiftResponse
R-->>U: RoomGiftSent MQ
U->>U: create or refresh CP application
U-->>N: UserCPApplicationCreated MQ
N->>IM: A -> B C2C cp_application_created
N->>IM: room cp_application_created, recipient_user_id=B
```
处理规则:
- 只有 `gift_type_code=cp``cp_relation_type` 有效时创建申请。
- 同一个 `RoomGiftSent.event_id` 只能消费一次。
- A 给 B 发送同一类型 CP 礼物时,如果已有 pending 申请,刷新礼物快照和 `expires_at_ms`
- A 给 B 发送不同类型 CP 礼物时,可以各有一条 pending 申请。
- 如果任一方已经有 active 关系,申请可以存在,但接受时会被拒绝;已有关系双方互送礼物会增加亲密值。
## 6. 同意和拒绝
```mermaid
sequenceDiagram
participant F as Flutter B
participant G as gateway-service
participant U as user-service
participant N as notice-service
participant IM as Tencent IM
F->>G: POST /api/v1/cp/applications/{id}/accept
G->>U: AcceptCPApplication
U->>U: lock pair + lock both users + create relationship
U-->>N: UserCPApplicationAccepted MQ
U-->>N: UserCPRelationshipCreated MQ
N->>IM: B -> A C2C cp_application_accepted
N->>IM: room cp_application_accepted, recipient_user_id=A
N->>IM: region cp_relationship_created
```
处理规则:
- 接受和拒绝都只能由申请目标用户 B 操作。
- pending 申请过期后不能接受gateway envelope 返回 `CONFLICT`;内部 gRPC 语义是 FailedPrecondition。
- 接受时同时校验 A 和 B 是否已经有任意 active 关系;任一方已有关系就返回 `CONFLICT`
- 同意成功后写 active 关系,关闭同一对用户其他 pending 申请。
- 同意或拒绝都向 A 发送 C2C IM 和房间 IM。
- 同意成功后额外发送区域 IM区域取申请来源房间的 `visible_region_id`
## 7. 亲密值和升级
任意礼物发送成功后,如果发送方和收礼方已经有 active 关系,则按本次礼物的 `heat_value` 增加亲密值。
规则:
- 普通礼物按 100% 热度值增加亲密值。
- 幸运礼物、超级幸运礼物直接使用钱包回执里的最终 `heat_value`,不重复计算比例。
- 多目标送礼时按每个目标的账务回执分别增加对应关系亲密值。
- 等级范围 1 到 5当前使用 user-service 默认等级规则,不提供后台配置入口。
- 每个等级可绑定奖励资源组,升级时调用 `wallet-service` 给双方发放资源。
- 奖励发放幂等键为 `cp_level_reward:{relationship_id}:{level}:{user_id}`
## 8. App HTTP接口
所有接口走 `gateway-service`,响应使用 `{code,message,request_id,data}`
| 方法 | 地址 | 说明 |
| --- | --- | --- |
| `GET` | `/api/v1/cp/applications` | 查询我的申请 |
| `POST` | `/api/v1/cp/applications/{application_id}/accept` | 同意申请 |
| `POST` | `/api/v1/cp/applications/{application_id}/reject` | 拒绝申请 |
| `GET` | `/api/v1/cp/relationships` | 查询我的 active 关系 |
申请列表参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `direction` | string | 否 | `incoming``outgoing`,默认 `incoming` |
| `status` | string | 否 | `pending``accepted``rejected``expired``blocked` |
| `page` | int32 | 否 | 默认 1 |
| `page_size` | int32 | 否 | 默认 20 |
关系列表参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `relation_type` | string | 否 | `cp``brother``sister`;不传返回全部 |
| `page` | int32 | 否 | 默认 1 |
| `page_size` | int32 | 否 | 默认 20 |
拒绝请求体:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `reason` | string | 否 | 拒绝原因,只进入 IM/outbox payload |
## 9. 数据表
表放在 `user-service` 库,所有表带 `app_code`,时间使用 UTC epoch milliseconds。
| 表 | 说明 |
| --- | --- |
| `user_cp_relation_configs` | 历史兼容表;当前固定 `max_count_per_user=1`,不提供后台配置 |
| `user_cp_level_rules` | 默认等级阈值和奖励资源组 |
| `user_cp_gift_event_consumption` | `RoomGiftSent` 消费幂等 |
| `user_cp_applications` | CP 关系申请 |
| `user_cp_relationships` | 双人关系事实 |
| `user_cp_level_reward_grants` | 等级奖励发放幂等 |
| `user_outbox` | CP 事件事实 |
关键唯一约束:
- active 关系按用户对唯一:`(app_code, user_a_id, user_b_id, status='active')`
- active 关系按单用户唯一:任一用户已有 active 关系就不能再接受新关系。
- pending 申请按用户对和类型唯一:`(app_code, requester_user_id, target_user_id, relation_type, status='pending')`
## 10. 相关IM
| IM | 通道 | 发送时机 |
| --- | --- | --- |
| `cp_application_created` | C2C | A 送 CP 礼物触发申请后A 发给 B |
| `cp_application_created` | 房间群 | A 送 CP 礼物触发申请后发到来源房间payload 带 `recipient_user_id=B` |
| `cp_application_accepted` | C2C | B 同意后B 发给 A |
| `cp_application_accepted` | 房间群 | B 同意后发到来源房间payload 带 `recipient_user_id=A` |
| `cp_application_rejected` | C2C | B 拒绝后B 发给 A |
| `cp_application_rejected` | 房间群 | B 拒绝后发到来源房间payload 带 `recipient_user_id=A` |
| `cp_relationship_created` | 区域群 | 关系建立后发送区域播报 |
C2C 使用腾讯云 IM `From_Account` 表达真实操作人:申请为 A处理结果为 B。区域 IM 群 ID 使用 `hy_<app_code>_bc_r_<region_id>`,本地测试可通过 `group_id_prefix` 加前缀。
## 11. 错误码
| 场景 | code | 说明 |
| --- | --- | --- |
| 申请不存在 | `NOT_FOUND` | application_id 不存在或不属于当前用户 |
| 申请已过期 | `CONFLICT` | 不能再同意;内部 gRPC 语义是 FailedPrecondition |
| 已有关系 | `CONFLICT` | A/B 任一方已经有 active 关系 |
| 无权限处理 | `PERMISSION_DENIED` | 不是申请目标用户 |
| 幂等冲突 | `CONFLICT` | command_id 或事件消费 payload 不一致 |
## 12. 验证项
- CP 礼物发送成功后,`RoomGiftSent``gift_type_code=cp``cp_relation_type`、礼物 icon 快照。
- A 给 B 连发 `cp``brother``sister` 三种礼物,会生成三条 pending 申请。
- B 接受其中一条后,其他两条不能再接受。
- A/B 建立关系后A 或 B 都不能再和第三人建立 active 关系。
- pending 申请超过默认 24 小时后不能接受。
- active 关系双方互送任意礼物,亲密值按 `heat_value` 增加。
- 同意、拒绝、关系成立的 IM 都写入 `notice_delivery_events`,失败可重试。

View File

@ -0,0 +1,119 @@
# CP关系本地真实链路测试用例
本文只记录本地真实链路验证用例。业务动作优先走 gateway HTTP、room/wallet/user/notice 服务和 RocketMQSQL 只用于准备本地测试数据和核对最终事实。
## 环境前置
- Docker 本地服务MySQL、Redis、RocketMQ、gateway-service、room-service、wallet-service、user-service、notice-service。
- `user-service` 必须启用 `cp_relationship_worker`,并消费 `hyapp_room_outbox`
- `notice-service` 必须消费 `hyapp_user_outbox`,并把 CP 申请/同意/拒绝/关系创建事件投递到腾讯云 IM。
- 本地测试账号 A、B、C 需要是同一个 `app_code=lalu` 下的完整资料用户。
- 本地 CP 礼物需要是 `gift_type_code=cp`,并在礼物配置中写入 `cp_relation_type`
- 送礼前 A 和 B 必须都在同一个房间的 room-service presence 内。
## T01 CP礼物创建申请
步骤:
1. 准备 A、B 用户和 A 的 COIN 余额。
2. 准备 CP 礼物 `local_cp_gift_cp``cp_relation_type=cp`
3. A 创建房间A 和 B 都调用进房接口。
4. A 通过 gateway 给 B 送 CP 礼物。
验证:
- `hyapp_room.room_outbox``RoomGiftSent`payload 中 `cp_relation_type=cp``gift_icon_url` 非空。
- RocketMQ `hyapp_room_outbox` 被 user-service CP consumer 消费。
- `hyapp_user.user_cp_applications` 有 A 到 B 的 pending 申请,`expires_at_ms` 为当前时间加服务端默认 24 小时。
- `hyapp_user.user_cp_gift_event_consumption` 有该 room event 的消费记录。
- `hyapp_user.user_outbox``UserCPApplicationCreated`
- 腾讯云 IM 有 A 发给 B 的 C2C 申请消息,`Desc=cp_application_created`payload 含 `application_id``relation_type=cp``expires_at_ms`
- 腾讯云 IM 有发到房间群的申请消息,`Desc=cp_application_created`payload 含 `recipient_user_id=B``room_id`
## T02 B同意申请建立关系
步骤:
1. 使用 T01 产生的 `application_id`
2. B 调用 gateway 同意申请接口。
验证:
- `user_cp_applications.status=accepted`
- `user_cp_relationships` 新增 active 关系A/B 无序 pair 只有一条 active 关系。
- 同一对用户其他 pending 申请被置为 `blocked`
- `user_outbox``UserCPApplicationAccepted``UserCPRelationshipCreated`
- 腾讯云 IM 向 A 发送 B 发出的同意结果 C2C 消息,`Desc=cp_application_accepted`
- 腾讯云 IM 向房间群发送同意结果消息,`Desc=cp_application_accepted`payload 含 `recipient_user_id=A`
- 腾讯云 IM 向区域群发送关系创建消息,`Desc=cp_relationship_created`payload 含 A/B 头像昵称短 ID、礼物 icon 和 `room_id`
## T03 已有关系后送礼增加亲密值并升级
步骤:
1. 使用服务端默认等级规则,确认 1 级阈值为 0。
2. A 再次给 B 送 CP 礼物。
验证:
- 不新增 pending 申请。
- 原关系 `intimacy_value` 增加礼物结算热度值。
- 原关系 `level_no` 按服务端默认阈值计算。
- `user_outbox``UserCPRelationshipIntimacyChanged`
## T04 同一对用户只能同时存在一种关系
步骤:
1. A 给 B 发送兄弟礼物,生成 brother pending 申请。
2. A 给 B 发送姐妹礼物,生成 sister pending 申请。
3. B 接受其中一个申请。
4. B 再尝试接受另一个申请。
验证:
- 第一个接受成功后产生 active 关系。
- 另一个申请不能接受,接口返回冲突提示。
- A/B active 关系仍只有一种。
## T05 同一用户只能同时存在一个关系
步骤:
1. 使用 T02 建立 A/B active 关系。
2. C 给 A 发送 CP 礼物,生成 C 到 A 的 pending 申请。
3. A 尝试同意 C 的申请。
验证:
- 同意接口返回冲突。
- A/B active 关系仍保留。
- C/A 不创建 active 关系。
## T06 申请过期
步骤:
1. 创建一条 pending 申请。
2. 通过 SQL 把 `expires_at_ms` 调整到当前时间之前。
3. B 查询申请列表或尝试同意。
验证:
- 申请状态被固化为 `expired`
- 同意接口返回 `CONFLICT`,申请被固化为 `expired`,不建立关系。
## T07 拒绝申请
步骤:
1. 创建一条 pending 申请。
2. B 调用拒绝接口。
验证:
- `user_cp_applications.status=rejected`
- 不创建 active 关系。
- `user_outbox``UserCPApplicationRejected`
- 腾讯云 IM 向 A 发送 B 发出的拒绝结果 C2C 消息,`Desc=cp_application_rejected`
- 腾讯云 IM 向房间群发送拒绝结果消息,`Desc=cp_application_rejected`payload 含 `recipient_user_id=A`

View File

@ -0,0 +1,455 @@
# CP关系Flutter对接
本文只写 App 需要对接的地址、参数、返回值和相关 IM。CP 关系包含 `cp``brother``sister` 三种类型;每个用户同时只能有一个 active 关系,不需要佩戴接口,也没有后台数量配置。
## 1. 礼物面板CP Tab
地址:
```http
GET /api/v1/rooms/{room_id}/gift-panel
Authorization: Bearer <access_token>
X-App-Code: lalu
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID |
返回值:
`tabs` 里会有 `gift_type_code=cp` 的 Tab。CP 礼物会带:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `gift_id` | string | 送礼时使用 |
| `gift_type_code` | string | CP 礼物为 `cp` |
| `cp_relation_type` | string | `cp``brother``sister` |
| `name` | string | 礼物名 |
| `coin_price` | int64 | 展示价格,扣费以后端为准 |
| `resource.asset_url` | string | 礼物 icon |
| `resource.animation_url` | string | 礼物动效,可为空 |
Flutter 在 CP 礼物 Tab 下按 `cp_relation_type` 分成 CP、兄弟、姐妹三个二级 Tab。
## 2. 发送CP礼物
地址:
```http
POST /api/v1/rooms/gift/send
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID |
| `command_id` | string | 是 | 本次送礼幂等键,重试必须复用 |
| `target_type` | string | 否 | 当前传 `user` |
| `target_user_ids` | int64[] | 是 | CP 礼物只传 1 个收礼人 |
| `gift_id` | string | 是 | CP 礼物 ID |
| `gift_count` | int32 | 是 | 必须大于 0 |
示例:
```json
{
"room_id": "room_1001",
"command_id": "cp_gift_1779259000000_10001_10002",
"target_type": "user",
"target_user_ids": [10002],
"gift_id": "cp_ring",
"gift_count": 1
}
```
返回值:
沿用普通送礼响应。关系申请通过 IM 到达,送礼响应不保证返回申请 ID。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `result.applied` | bool | 本次命令是否落地 |
| `billing_receipt_id` | string | 扣费回执 |
| `room_heat` | int64 | 最新房间热度 |
| `gift_rank` | array | 房间礼物榜 |
| `room` | object | 房间快照 |
| `lucky_gift` | object | 幸运礼物结果,普通 CP 礼物为空 |
## 3. 查询我的申请
地址:
```http
GET /api/v1/cp/applications?direction=incoming&status=pending&page=1&page_size=20
Authorization: Bearer <access_token>
X-App-Code: lalu
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `direction` | string | 否 | `incoming` 我收到的;`outgoing` 我发出的 |
| `status` | string | 否 | `pending``accepted``rejected``expired``blocked` |
| `page` | int32 | 否 | 默认 1 |
| `page_size` | int32 | 否 | 默认 20 |
返回值:
```json
{
"applications": [
{
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"status": "pending",
"requester": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"target": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"room_id": "room_1001",
"room_region_id": 86,
"gift": {
"gift_id": "cp_ring",
"gift_name": "CP Ring",
"gift_icon_url": "https://cdn.example/cp-ring.png",
"gift_animation_url": "",
"gift_count": 1,
"gift_value": 520,
"billing_receipt_id": "gift_tx_1"
},
"created_at_ms": 1779259000000,
"updated_at_ms": 1779259000000,
"expires_at_ms": 1779345400000,
"decided_at_ms": 0
}
],
"total": 1,
"server_time_ms": 1779259001000
}
```
## 4. 同意申请
地址:
```http
POST /api/v1/cp/applications/{application_id}/accept
Authorization: Bearer <access_token>
X-App-Code: lalu
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `application_id` | string | 是 | 申请 ID |
返回值:
```json
{
"application": {
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"status": "accepted",
"decided_at_ms": 1779259100000
},
"relationship": {
"relationship_id": "cp_rel_10001_10002",
"relation_type": "cp",
"status": "active",
"me": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"partner": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"intimacy_value": 0,
"level": 1,
"formed_at_ms": 1779259100000,
"updated_at_ms": 1779259100000
}
}
```
错误处理:
| code | 处理 |
| --- | --- |
| `CONFLICT` | A/B 任一方已经有 active 关系,或申请已过期;刷新申请和关系列表 |
| `NOT_FOUND` | 申请不存在,刷新申请列表 |
| `PERMISSION_DENIED` | 当前用户不是申请目标用户 |
## 5. 拒绝申请
地址:
```http
POST /api/v1/cp/applications/{application_id}/reject
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `application_id` | string | 是 | 申请 ID |
| `reason` | string | 否 | 拒绝原因 |
返回值:
```json
{
"application": {
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"status": "rejected",
"decided_at_ms": 1779259100000
}
}
```
## 6. 查询我的关系
地址:
```http
GET /api/v1/cp/relationships?relation_type=cp&page=1&page_size=20
Authorization: Bearer <access_token>
X-App-Code: lalu
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `relation_type` | string | 否 | `cp``brother``sister`;不传返回全部 |
| `page` | int32 | 否 | 默认 1 |
| `page_size` | int32 | 否 | 默认 20 |
返回值:
```json
{
"relationships": [
{
"relationship_id": "cp_rel_10001_10002",
"relation_type": "cp",
"status": "active",
"me": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"partner": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"intimacy_value": 1888,
"level": 2,
"formed_at_ms": 1779259100000,
"updated_at_ms": 1779259200000
}
],
"total": 1
}
```
说明:
- 每个用户同时只能有一个 active 关系。
- 不需要佩戴接口,资料卡直接展示当前 active 关系。
## 7. 相关IM
所有 IM 都是 Tencent `TIMCustomElem``MsgContent.Desc` 等于下表事件名,`MsgContent.Data` 是 JSON 字符串。客户端按 `event_id` 去重。
### 7.1 收到关系申请
房间 IM 和 C2C IM 都会收到:
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `cp_application_created` |
| `Data.event_type` | `cp_application_created` |
Data
```json
{
"event_id": "cp_application:created:10002:1",
"event_type": "cp_application_created",
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"status": "pending",
"requester": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"target": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"room_id": "room_1001",
"room_region_id": 86,
"gift": {
"gift_id": "cp_ring",
"gift_name": "CP Ring",
"gift_icon_url": "https://cdn.example/cp-ring.png",
"gift_animation_url": "",
"gift_count": 1,
"gift_value": 520,
"billing_receipt_id": "gift_tx_1"
},
"expires_at_ms": 1779345400000,
"decided_at_ms": 0,
"source_created_at_ms": 1779259000000,
"recipient_user_id": "10002"
}
```
处理:
- 房间 IM 只有 `recipient_user_id` 等于当前用户时显示同意/拒绝按钮。
- C2C IM 在私聊里显示同意/拒绝按钮。
- `expires_at_ms <= server_time_ms` 时按钮置灰并刷新申请列表。
### 7.2 申请被同意
C2C 和房间 IM 都会收到:
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `cp_application_accepted` |
| `Data.event_type` | `cp_application_accepted` |
Data 字段和申请消息一致,差异是:
| 字段 | 值 |
| --- | --- |
| `status` | `accepted` |
| `decided_at_ms` | 处理时间 |
| `recipient_user_id` | 房间 IM 中为 A 的 user_id |
处理:
- A 收到后刷新 `/api/v1/cp/relationships`
- B 操作成功后也刷新关系列表。
- 同一对用户其他 pending 申请需要置灰。
### 7.3 申请被拒绝
C2C 和房间 IM 都会收到:
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `cp_application_rejected` |
| `Data.event_type` | `cp_application_rejected` |
Data 字段和申请消息一致,差异是:
| 字段 | 值 |
| --- | --- |
| `status` | `rejected` |
| `reason` | 拒绝原因,可为空 |
| `decided_at_ms` | 处理时间 |
| `recipient_user_id` | 房间 IM 中为 A 的 user_id |
### 7.4 区域关系成立播报
用户组成任意关系后,会发送区域 IM
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `cp_relationship_created` |
| `Data.event_type` | `cp_relationship_created` |
Data
```json
{
"event_id": "cp_relationship:created:10002:1",
"event_type": "cp_relationship_created",
"relationship_id": "cp_rel_10001_10002",
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"requester": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"target": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"gift": {
"gift_id": "cp_ring",
"gift_name": "CP Ring",
"gift_icon_url": "https://cdn.example/cp-ring.png",
"gift_animation_url": "",
"gift_count": 1,
"gift_value": 520,
"billing_receipt_id": "gift_tx_1"
},
"room_id": "room_1001",
"room_region_id": 86,
"group_id": "hy_lalu_bc_r_86",
"source_created_at_ms": 1779259100000
}
```
处理:
- 只在区域播报位展示,不当作私聊消息。
- 点击可进入 `room_id` 对应房间;房间不存在或进房失败时只关闭播报。
## 8. 客户端错误提示
| code | 建议提示 |
| --- | --- |
| `CONFLICT` | `你或对方已经有关系`;如果申请已过期则提示 `申请已过期` |
| `NOT_FOUND` | `申请不存在或已失效` |
| `PERMISSION_DENIED` | `只能处理发给你的申请` |
## 9. 本地状态规则
- 不要靠送礼成功直接创建关系。
- 不要靠 C2C 或房间按钮本地改关系,必须调用同意或拒绝接口。
- 同一个 `event_id` 只处理一次。
- 同一个 `application_id` 在房间和 C2C 里可能都出现,按钮状态要共享。
- 申请过期时间以 `expires_at_ms` 为准。
- 亲密值和等级以后端关系接口返回为准,不在 Flutter 里按礼物价格推算。

View File

@ -0,0 +1,179 @@
# Flutter 心跳接口对接
## 1. APP 心跳
### 接口地址
```http
POST /api/v1/app/heartbeat
Authorization: Bearer <access_token>
```
### 参数
Body 不需要传参数。
Header
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `Authorization` | 是 | 登录 token。 |
| `X-Device-ID` | 否 | 设备 ID有就传服务端只做会话审计。 |
### 返回值
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"accepted": true,
"heartbeat_at_ms": 1778000005000,
"server_time_ms": 1778000005000
}
}
```
| 字段 | 说明 |
| --- | --- |
| `data.accepted` | 是否接受本次 APP 心跳。 |
| `data.heartbeat_at_ms` | 本次 APP 心跳写入时间UTC epoch milliseconds。 |
| `data.server_time_ms` | 服务端时间UTC epoch milliseconds。 |
相关 IM无。
## 2. 房间心跳
### 接口地址
```http
POST /api/v1/rooms/heartbeat
Authorization: Bearer <access_token>
Content-Type: application/json
```
### 参数
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 房间 ID。 |
| `command_id` | string | 是 | 本次心跳命令 ID。同一次请求重试复用同一个值下一次心跳换新的值。 |
请求示例:
```json
{
"room_id": "lalu_xxx",
"command_id": "cmd_room_heartbeat_lalu_xxx_123_001"
}
```
### 返回值
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"result": {
"applied": true,
"room_version": 18,
"server_time_ms": 1778000005000
},
"user": {
"user_id": 123,
"role": "audience",
"joined_at_ms": 1778000000000,
"last_seen_at_ms": 1778000005000
},
"room": {
"room_id": "lalu_xxx",
"version": 18,
"online_users": []
}
}
}
```
| 字段 | 说明 |
| --- | --- |
| `data.result.applied` | 本次心跳是否写入成功。 |
| `data.result.room_version` | 房间版本。 |
| `data.user.last_seen_at_ms` | 当前用户房间 presence 最近刷新时间。 |
| `data.room` | 刷新后的房间快照。 |
相关 IM无。房间心跳只刷新 room-service presence不会广播独立 IM。
## 3. 麦位心跳
### 接口地址
```http
POST /api/v1/rooms/mic/heartbeat
Authorization: Bearer <access_token>
Content-Type: application/json
```
### 参数
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 房间 ID。 |
| `command_id` | string | 是 | 本次麦位心跳命令 ID。同一次请求重试复用同一个值下一次心跳换新的值。 |
| `mic_session_id` | string | 是 | 当前麦位会话 ID必须使用上麦返回的 `mic_session_id`。 |
| `target_user_id` | int64 | 否 | 传 `0` 或不传表示当前登录用户自己。 |
请求示例:
```json
{
"room_id": "lalu_xxx",
"command_id": "cmd_mic_heartbeat_lalu_xxx_123_001",
"mic_session_id": "mic_xxx",
"target_user_id": 0
}
```
### 返回值
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"result": {
"applied": true,
"room_version": 18,
"server_time_ms": 1778000005000
},
"seat_no": 1,
"mic_heartbeat_at_ms": 1778000005000,
"room": {
"room_id": "lalu_xxx",
"version": 18,
"mic_seats": [
{
"seat_no": 1,
"user_id": 123,
"publish_state": "publishing",
"mic_session_id": "mic_xxx",
"mic_heartbeat_at_ms": 1778000005000
}
]
}
}
}
```
| 字段 | 说明 |
| --- | --- |
| `data.seat_no` | 当前用户所在麦位。 |
| `data.mic_heartbeat_at_ms` | 服务端接受本次麦位心跳的时间UTC epoch milliseconds。 |
| `data.room` | 刷新后的房间快照。 |
| `data.room.mic_seats[].mic_heartbeat_at_ms` | 麦位最近一次服务端接受心跳的时间。 |
| `data.room.mic_seats[].publish_state` | `publishing` 表示当前会话已确认发流,可以继续麦位心跳。 |
相关 IM无独立心跳 IM。收到 `room_mic_down` 后停止麦位心跳。

View File

@ -18,6 +18,7 @@
| GET | `/api/v1/countries` | countries | `listRegistrationCountries` | 获取 App 注册页可选国家 |
| GET | `/api/v1/app/bootstrap` | app-config | `getAppBootstrap` | 获取 App 启动配置摘要 |
| GET | `/api/v1/app/h5-links` | app-config | `listAppH5Links` | 获取 App H5 入口地址 |
| POST | `/api/v1/app/heartbeat` | app | `appHeartbeat` | 刷新当前 App 登录会话心跳 |
| GET | `/api/v1/im/usersig` | tencent-im | `issueTencentIMUserSig` | 获取腾讯云 IM UserSig |
| POST | `/api/v1/rtc/token` | tencent-rtc | `issueTencentRTCToken` | 获取腾讯 RTC 进房 UserSig |
| POST | `/api/v1/tencent-rtc/callback` | tencent-rtc | `handleTencentRTCCallback` | 腾讯 RTC 服务端事件回调 |
@ -75,6 +76,7 @@
| POST | `/api/v1/rooms/mic/down` | rooms | `micDown` | 下麦 |
| POST | `/api/v1/rooms/mic/change` | rooms | `changeMicSeat` | 调整麦位 |
| POST | `/api/v1/rooms/mic/publishing/confirm` | rooms | `confirmMicPublishing` | 确认 RTC 音频发布成功 |
| POST | `/api/v1/rooms/mic/heartbeat` | rooms | `micHeartbeat` | 刷新当前麦位会话心跳 |
| POST | `/api/v1/rooms/mic/mute` | rooms | `setMicMute` | 设置服务端可见麦克风静音状态 |
| POST | `/api/v1/rooms/mic/lock` | rooms | `setMicSeatLock` | 锁定或解锁麦位 |
| POST | `/api/v1/rooms/chat/enabled` | rooms | `setChatEnabled` | 开启或关闭房间公屏 |

View File

@ -32,6 +32,7 @@ import (
authmodule "hyapp-admin-server/internal/modules/auth"
coinledgermodule "hyapp-admin-server/internal/modules/coinledger"
countryregionmodule "hyapp-admin-server/internal/modules/countryregion"
cprelationmodule "hyapp-admin-server/internal/modules/cprelation"
cumulativerechargerewardmodule "hyapp-admin-server/internal/modules/cumulativerechargereward"
dailytaskmodule "hyapp-admin-server/internal/modules/dailytask"
dashboardmodule "hyapp-admin-server/internal/modules/dashboard"
@ -230,6 +231,7 @@ func main() {
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),
CumulativeRecharge: cumulativerechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
DailyTask: dailytaskmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
Dashboard: dashboardmodule.New(store, cfg),

View File

@ -0,0 +1,50 @@
package cprelation
import (
"errors"
"fmt"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
"github.com/gin-gonic/gin"
)
type Handler struct {
service *Service
audit shared.OperationLogger
}
func New(userDB DB, audit shared.OperationLogger) *Handler {
return &Handler{service: NewService(userDB), audit: audit}
}
func (h *Handler) GetConfig(c *gin.Context) {
config, err := h.service.GetConfig(c.Request.Context(), appctx.FromContext(c.Request.Context()))
if err != nil {
response.ServerError(c, "获取CP配置失败")
return
}
response.OK(c, config)
}
func (h *Handler) UpdateConfig(c *gin.Context) {
var req updateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "CP配置参数不正确")
return
}
config, err := h.service.UpdateConfig(c.Request.Context(), appctx.FromContext(c.Request.Context()), req)
if err != nil {
if errors.Is(err, ErrInvalidArgument) {
response.BadRequest(c, err.Error())
return
}
response.ServerError(c, "更新CP配置失败")
return
}
shared.OperationLogWithResourceID(c, h.audit, "update-cp-config", "user_cp_relation_configs", config.AppCode, "success",
fmt.Sprintf("relations=%d", len(config.Relations)))
response.OK(c, config)
}

View File

@ -0,0 +1,15 @@
package cprelation
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/cp/config", middleware.RequirePermission("cp-config:view"), h.GetConfig)
protected.PUT("/admin/activity/cp/config", middleware.RequirePermission("cp-config:update"), h.UpdateConfig)
}

View File

@ -0,0 +1,373 @@
package cprelation
import (
"context"
"database/sql"
"errors"
"fmt"
"slices"
"strings"
"time"
)
const (
relationTypeCP = "cp"
relationTypeBrother = "brother"
relationTypeSister = "sister"
statusActive = "active"
levelCount = 5
defaultExpireHours = 24
)
var (
ErrInvalidArgument = errors.New("invalid argument")
relationTypes = []string{relationTypeCP, relationTypeBrother, relationTypeSister}
relationLabels = map[string]string{
relationTypeCP: "CP",
relationTypeBrother: "兄弟",
relationTypeSister: "姐妹",
}
defaultLevelThresholds = []int64{0, 10_000, 30_000, 60_000, 100_000}
)
type DB interface {
BeginTx(context.Context, *sql.TxOptions) (*sql.Tx, error)
ExecContext(context.Context, string, ...any) (sql.Result, error)
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...any) *sql.Row
}
type Service struct {
db DB
}
type configDTO struct {
AppCode string `json:"appCode"`
Relations []relationDTO `json:"relations"`
ServerTimeMS int64 `json:"serverTimeMs"`
}
type relationDTO struct {
RelationType string `json:"relationType"`
DisplayName string `json:"displayName"`
MaxCountPerUser int32 `json:"maxCountPerUser"`
ApplicationExpireHours int32 `json:"applicationExpireHours"`
Status string `json:"status"`
Levels []levelDTO `json:"levels"`
UpdatedAtMS int64 `json:"updatedAtMs"`
}
type levelDTO struct {
Level int32 `json:"level"`
IntimacyThreshold int64 `json:"intimacyThreshold"`
RewardResourceGroupID int64 `json:"rewardResourceGroupId"`
LevelIconURL string `json:"levelIconUrl"`
Status string `json:"status"`
UpdatedAtMS int64 `json:"updatedAtMs"`
}
type updateRequest struct {
Relations []relationDTO `json:"relations"`
}
func NewService(db DB) *Service {
return &Service{db: db}
}
// GetConfig 读取当前 App 的关系容量和等级阈值;缺配置时先落默认值,保证后台首次打开就是可保存状态。
func (s *Service) GetConfig(ctx context.Context, appCode string) (configDTO, error) {
nowMs := time.Now().UTC().UnixMilli()
if err := s.ensureSchema(ctx); err != nil {
return configDTO{}, err
}
if err := s.ensureDefaultConfig(ctx, appCode, nowMs); err != nil {
return configDTO{}, err
}
relations, err := s.listRelations(ctx, appCode)
if err != nil {
return configDTO{}, err
}
return configDTO{AppCode: appCode, Relations: relations, ServerTimeMS: nowMs}, nil
}
// UpdateConfig 覆盖低频配置;写入时按 relation_type + level_no upsert运行中消费逻辑每次读取最新阈值。
func (s *Service) UpdateConfig(ctx context.Context, appCode string, req updateRequest) (configDTO, error) {
nowMs := time.Now().UTC().UnixMilli()
if err := s.ensureSchema(ctx); err != nil {
return configDTO{}, err
}
relations, err := normalizeRelations(req.Relations)
if err != nil {
return configDTO{}, err
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return configDTO{}, err
}
defer func() { _ = tx.Rollback() }()
for _, relation := range relations {
if _, err := tx.ExecContext(ctx, `
INSERT INTO user_cp_relation_configs (
app_code, relation_type, max_count_per_user, application_expire_hours,
status, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
max_count_per_user = VALUES(max_count_per_user),
application_expire_hours = VALUES(application_expire_hours),
status = VALUES(status),
updated_at_ms = VALUES(updated_at_ms)`,
appCode, relation.RelationType, relation.MaxCountPerUser, relation.ApplicationExpireHours,
relation.Status, nowMs, nowMs,
); err != nil {
return configDTO{}, err
}
for _, level := range relation.Levels {
if _, err := tx.ExecContext(ctx, `
INSERT INTO user_cp_level_rules (
app_code, relation_type, level_no, intimacy_threshold,
reward_resource_group_id, level_icon_url, status, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
intimacy_threshold = VALUES(intimacy_threshold),
reward_resource_group_id = VALUES(reward_resource_group_id),
level_icon_url = VALUES(level_icon_url),
status = VALUES(status),
updated_at_ms = VALUES(updated_at_ms)`,
appCode, relation.RelationType, level.Level, level.IntimacyThreshold,
level.RewardResourceGroupID, level.LevelIconURL, level.Status, nowMs, nowMs,
); err != nil {
return configDTO{}, err
}
}
}
if err := tx.Commit(); err != nil {
return configDTO{}, err
}
return s.GetConfig(ctx, appCode)
}
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, status, updated_at_ms
FROM user_cp_relation_configs
WHERE app_code = ?`, appCode)
if err != nil {
return nil, err
}
defer rows.Close()
relationsByType := map[string]relationDTO{}
for rows.Next() {
var item relationDTO
if err := rows.Scan(&item.RelationType, &item.MaxCountPerUser, &item.ApplicationExpireHours, &item.Status, &item.UpdatedAtMS); err != nil {
return nil, err
}
item.DisplayName = relationLabels[item.RelationType]
relationsByType[item.RelationType] = item
}
if err := rows.Err(); err != nil {
return nil, err
}
levelsByType, err := s.levelsByRelationType(ctx, appCode)
if err != nil {
return nil, err
}
for relationType, levels := range levelsByType {
item := relationsByType[relationType]
item.Levels = levels
relationsByType[relationType] = item
}
relations := make([]relationDTO, 0, len(relationTypes))
for _, relationType := range relationTypes {
item := relationsByType[relationType]
item.RelationType = relationType
item.DisplayName = relationLabels[relationType]
item.Levels = normalizeLevelDefaults(item.Levels)
relations = append(relations, item)
}
return relations, nil
}
// levelsByRelationType 必须把读取失败直接抛回 handler后台配置页不能用默认等级掩盖坏 SQL 或坏数据。
func (s *Service) levelsByRelationType(ctx context.Context, appCode string) (map[string][]levelDTO, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT relation_type, level_no, intimacy_threshold, reward_resource_group_id, level_icon_url, status, updated_at_ms
FROM user_cp_level_rules
WHERE app_code = ?
ORDER BY relation_type ASC, level_no ASC`, appCode)
if err != nil {
return nil, err
}
defer rows.Close()
result := map[string][]levelDTO{}
for rows.Next() {
var relationType string
var item levelDTO
if err := rows.Scan(&relationType, &item.Level, &item.IntimacyThreshold, &item.RewardResourceGroupID, &item.LevelIconURL, &item.Status, &item.UpdatedAtMS); err != nil {
return nil, err
}
result[relationType] = append(result[relationType], item)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
}
func (s *Service) ensureDefaultConfig(ctx context.Context, appCode string, nowMs int64) error {
for _, relationType := range relationTypes {
maxCount := int32(1)
if relationType == relationTypeBrother || relationType == relationTypeSister {
maxCount = 5
}
if _, err := s.db.ExecContext(ctx, `
INSERT INTO user_cp_relation_configs (
app_code, relation_type, max_count_per_user, application_expire_hours,
status, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, 'active', ?, ?)
ON DUPLICATE KEY UPDATE relation_type = VALUES(relation_type)`,
appCode, relationType, maxCount, defaultExpireHours, nowMs, nowMs,
); err != nil {
return err
}
for level := int32(1); level <= levelCount; level++ {
if _, err := s.db.ExecContext(ctx, `
INSERT INTO user_cp_level_rules (
app_code, relation_type, level_no, intimacy_threshold,
reward_resource_group_id, level_icon_url, status, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, 0, '', 'active', ?, ?)
ON DUPLICATE KEY UPDATE level_no = VALUES(level_no)`,
appCode, relationType, level, defaultLevelThresholds[level-1], nowMs, nowMs,
); err != nil {
return err
}
}
}
return nil
}
func (s *Service) ensureSchema(ctx context.Context) error {
if s == nil || s.db == nil {
return errors.New("user db is not configured")
}
if _, err := s.db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS user_cp_relation_configs (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
relation_type VARCHAR(32) NOT NULL,
max_count_per_user INT NOT NULL,
application_expire_hours INT NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'active',
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, relation_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`); err != nil {
return err
}
_, err := s.db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS user_cp_level_rules (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
relation_type VARCHAR(32) NOT NULL,
level_no INT NOT NULL,
intimacy_threshold BIGINT NOT NULL,
reward_resource_group_id BIGINT NOT NULL DEFAULT 0,
level_icon_url VARCHAR(512) NOT NULL DEFAULT '',
status VARCHAR(32) NOT NULL DEFAULT 'active',
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, relation_type, level_no),
KEY idx_user_cp_level_rules_status (app_code, status, relation_type, level_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`)
return err
}
func normalizeRelations(input []relationDTO) ([]relationDTO, error) {
if len(input) == 0 {
return nil, fmt.Errorf("%w: 关系配置不能为空", ErrInvalidArgument)
}
seen := map[string]bool{}
out := make([]relationDTO, 0, len(relationTypes))
for _, relation := range input {
relation.RelationType = strings.ToLower(strings.TrimSpace(relation.RelationType))
if !slices.Contains(relationTypes, relation.RelationType) {
return nil, fmt.Errorf("%w: 关系类型不正确", ErrInvalidArgument)
}
if seen[relation.RelationType] {
return nil, fmt.Errorf("%w: 关系类型不能重复", ErrInvalidArgument)
}
seen[relation.RelationType] = true
// CP 关系仍是硬性一人一个;后台页面只允许调整兄弟/姐妹数量,接口层也强制收敛防止绕过前端。
if relation.RelationType == relationTypeCP {
relation.MaxCountPerUser = 1
}
if relation.MaxCountPerUser <= 0 {
return nil, fmt.Errorf("%w: %s数量必须大于0", ErrInvalidArgument, relationLabels[relation.RelationType])
}
if relation.ApplicationExpireHours <= 0 {
relation.ApplicationExpireHours = defaultExpireHours
}
relation.Status = defaultString(strings.TrimSpace(relation.Status), statusActive)
relation.Levels = normalizeLevelDefaults(relation.Levels)
if err := validateLevels(relation.Levels); err != nil {
return nil, err
}
out = append(out, relation)
}
for _, relationType := range relationTypes {
if !seen[relationType] {
return nil, fmt.Errorf("%w: 缺少%s配置", ErrInvalidArgument, relationLabels[relationType])
}
}
slices.SortFunc(out, func(left, right relationDTO) int {
return slices.Index(relationTypes, left.RelationType) - slices.Index(relationTypes, right.RelationType)
})
return out, nil
}
func normalizeLevelDefaults(input []levelDTO) []levelDTO {
byLevel := map[int32]levelDTO{}
for _, level := range input {
byLevel[level.Level] = level
}
levels := make([]levelDTO, 0, levelCount)
for levelNo := int32(1); levelNo <= levelCount; levelNo++ {
level := byLevel[levelNo]
level.Level = levelNo
level.Status = defaultString(strings.TrimSpace(level.Status), statusActive)
if level.IntimacyThreshold == 0 && levelNo > 1 {
level.IntimacyThreshold = defaultLevelThresholds[levelNo-1]
}
level.LevelIconURL = strings.TrimSpace(level.LevelIconURL)
levels = append(levels, level)
}
return levels
}
func validateLevels(levels []levelDTO) error {
if len(levels) != levelCount {
return fmt.Errorf("%w: 等级必须配置1到5级", ErrInvalidArgument)
}
previous := int64(-1)
for _, level := range levels {
if level.Level < 1 || level.Level > levelCount {
return fmt.Errorf("%w: 等级只能是1到5", ErrInvalidArgument)
}
if level.IntimacyThreshold < 0 {
return fmt.Errorf("%w: 第%d级亲密值不能小于0", ErrInvalidArgument, level.Level)
}
if level.IntimacyThreshold <= previous {
return fmt.Errorf("%w: 等级亲密值必须递增", ErrInvalidArgument)
}
if level.RewardResourceGroupID < 0 {
return fmt.Errorf("%w: 奖励资源组不能小于0", ErrInvalidArgument)
}
previous = level.IntimacyThreshold
}
return nil
}
func defaultString(value string, fallback string) string {
if strings.TrimSpace(value) == "" {
return fallback
}
return strings.TrimSpace(value)
}

View File

@ -0,0 +1,61 @@
package cprelation
import (
"errors"
"testing"
)
func TestNormalizeRelationsForcesCPMaxCountAndKeepsSiblingLimits(t *testing.T) {
relations, err := normalizeRelations([]relationDTO{
relationInput(relationTypeCP, 99),
relationInput(relationTypeBrother, 7),
relationInput(relationTypeSister, 8),
})
if err != nil {
t.Fatalf("normalizeRelations() error = %v", err)
}
got := map[string]int32{}
for _, relation := range relations {
got[relation.RelationType] = relation.MaxCountPerUser
}
if got[relationTypeCP] != 1 {
t.Fatalf("CP max count = %d, want 1", got[relationTypeCP])
}
if got[relationTypeBrother] != 7 {
t.Fatalf("brother max count = %d, want 7", got[relationTypeBrother])
}
if got[relationTypeSister] != 8 {
t.Fatalf("sister max count = %d, want 8", got[relationTypeSister])
}
}
func TestNormalizeRelationsRequiresIncreasingLevelThresholds(t *testing.T) {
input := relationInput(relationTypeBrother, 5)
input.Levels[2].IntimacyThreshold = input.Levels[1].IntimacyThreshold
_, err := normalizeRelations([]relationDTO{
relationInput(relationTypeCP, 1),
input,
relationInput(relationTypeSister, 5),
})
if !errors.Is(err, ErrInvalidArgument) {
t.Fatalf("normalizeRelations() error = %v, want ErrInvalidArgument", err)
}
}
func relationInput(relationType string, maxCount int32) relationDTO {
return relationDTO{
RelationType: relationType,
MaxCountPerUser: maxCount,
ApplicationExpireHours: defaultExpireHours,
Status: statusActive,
Levels: []levelDTO{
{Level: 1, IntimacyThreshold: 0, Status: statusActive},
{Level: 2, IntimacyThreshold: 10_000, Status: statusActive},
{Level: 3, IntimacyThreshold: 30_000, Status: statusActive},
{Level: 4, IntimacyThreshold: 60_000, Status: statusActive},
{Level: 5, IntimacyThreshold: 100_000, Status: statusActive},
},
}
}

View File

@ -121,6 +121,8 @@ var defaultPermissions = []model.Permission{
{Name: "用户榜单查看", Code: "user-leaderboard:view", Kind: "menu"},
{Name: "红包配置查看", Code: "red-packet:view", Kind: "menu"},
{Name: "红包配置更新", Code: "red-packet:update", Kind: "button"},
{Name: "CP 配置查看", Code: "cp-config:view", Kind: "menu"},
{Name: "CP 配置更新", Code: "cp-config:update", Kind: "button"},
{Name: "VIP 配置查看", Code: "vip-config:view", Kind: "menu"},
{Name: "VIP 配置更新", Code: "vip-config:update", Kind: "button"},
{Name: "VIP 赠送", Code: "vip-config:grant", Kind: "button"},
@ -270,7 +272,8 @@ func (s *Store) seedMenus() error {
{ParentID: &activityID, Title: "房间火箭", Code: "room-rocket", Path: "/activities/room-rocket", Icon: "redeem", PermissionCode: "room-rocket:view", Sort: 73, Visible: true},
{ParentID: &activityID, Title: "用户榜单", Code: "user-leaderboard", Path: "/activities/user-leaderboards", Icon: "leaderboard", PermissionCode: "user-leaderboard:view", Sort: 74, Visible: true},
{ParentID: &activityID, Title: "红包配置", Code: "red-packet", Path: "/activities/red-packets", Icon: "redeem", PermissionCode: "red-packet:view", Sort: 75, Visible: true},
{ParentID: &activityID, Title: "VIP配置", Code: "vip-config", Path: "/activities/vip-config", Icon: "workspace_premium", PermissionCode: "vip-config:view", Sort: 76, Visible: true},
{ParentID: &activityID, Title: "CP配置", Code: "cp-config", Path: "/activities/cp-config", Icon: "favorite", PermissionCode: "cp-config:view", Sort: 76, Visible: true},
{ParentID: &activityID, Title: "VIP配置", Code: "vip-config", Path: "/activities/vip-config", Icon: "workspace_premium", PermissionCode: "vip-config:view", Sort: 77, Visible: true},
{ParentID: &activityID, Title: "周星配置", Code: "weekly-star", Path: "/activities/weekly-star", Icon: "star", PermissionCode: "weekly-star:view", Sort: 78, Visible: true},
{ParentID: &gameID, Title: "游戏列表", Code: "game-list", Path: "/games", Icon: "sports_esports", PermissionCode: "game:view", Sort: 70, Visible: true},
{ParentID: &geoID, Title: "国家管理", Code: "host-org-countries", Path: "/host/countries", Icon: "public", PermissionCode: "country:view", Sort: 70, Visible: true},
@ -517,6 +520,7 @@ func defaultRolePermissionCodes(code string) []string {
"seven-day-checkin:view", "seven-day-checkin:update",
"room-rocket:view", "room-rocket:update",
"red-packet:view", "red-packet:update",
"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",
"log:view",
@ -524,7 +528,7 @@ func defaultRolePermissionCodes(code string) []string {
"upload:create",
}
case "auditor":
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config: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-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet: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", "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-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"}
case "readonly":
return []string{
"overview:view",
@ -566,6 +570,7 @@ func defaultRolePermissionCodes(code string) []string {
"seven-day-checkin:view",
"room-rocket:view",
"red-packet:view",
"cp-config:view",
"vip-config:view",
"weekly-star:view",
"role:view",
@ -608,11 +613,12 @@ func defaultRolePermissionMigrationCodes(code string) []string {
"seven-day-checkin:view", "seven-day-checkin:update",
"room-rocket:view", "room-rocket:update",
"red-packet:view", "red-packet:update",
"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",
}
case "auditor", "readonly":
return []string{"level-config: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-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "vip-config:view", "weekly-star:view"}
return []string{"level-config: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-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"}
default:
return nil
}

View File

@ -12,6 +12,7 @@ import (
authroutes "hyapp-admin-server/internal/modules/auth"
"hyapp-admin-server/internal/modules/coinledger"
"hyapp-admin-server/internal/modules/countryregion"
"hyapp-admin-server/internal/modules/cprelation"
"hyapp-admin-server/internal/modules/cumulativerechargereward"
"hyapp-admin-server/internal/modules/dailytask"
"hyapp-admin-server/internal/modules/dashboard"
@ -60,6 +61,7 @@ type Handlers struct {
Auth *authroutes.Handler
CoinLedger *coinledger.Handler
CountryRegion *countryregion.Handler
CPRelation *cprelation.Handler
CumulativeRecharge *cumulativerechargereward.Handler
DailyTask *dailytask.Handler
Dashboard *dashboard.Handler
@ -118,6 +120,7 @@ func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine {
appregistry.RegisterRoutes(protected, h.AppRegistry)
appconfig.RegisterRoutes(protected, h.AppConfig)
countryregion.RegisterRoutes(protected, h.CountryRegion)
cprelation.RegisterRoutes(protected, h.CPRelation)
cumulativerechargereward.RegisterRoutes(protected, h.CumulativeRecharge)
dailytask.RegisterRoutes(protected, h.DailyTask)
firstrechargereward.RegisterRoutes(protected, h.FirstRechargeReward)

View File

@ -0,0 +1,59 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CP配置属于活动管理低频运营项菜单权限给查看保存动作单独用更新权限控制。
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
('CP 配置查看', 'cp-config:view', 'menu', '', @now_ms, @now_ms),
('CP 配置更新', 'cp-config: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) VALUES
(NULL, '活动管理', 'activities', '', 'campaign', '', 90, TRUE, @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
title = VALUES(title),
path = VALUES(path),
icon = VALUES(icon),
permission_code = VALUES(permission_code),
sort = VALUES(sort),
visible = VALUES(visible),
updated_at_ms = @now_ms;
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
SELECT parent.id, 'CP配置', 'cp-config', '/activities/cp-config', 'favorite', 'cp-config:view', 76, TRUE, @now_ms, @now_ms
FROM admin_menus parent
WHERE parent.code = 'activities'
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
title = VALUES(title),
path = VALUES(path),
icon = VALUES(icon),
permission_code = VALUES(permission_code),
sort = VALUES(sort),
visible = VALUES(visible),
updated_at_ms = @now_ms;
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code = 'platform-admin'
AND admin_permission.code IN ('cp-config:view', 'cp-config: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 = 'ops-admin'
AND admin_permission.code IN ('cp-config:view', 'cp-config: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 = 'cp-config:view';

View File

@ -269,7 +269,6 @@ func (s *Service) upsertCycle(ctx context.Context, command domain.CycleCommand)
return domain.Cycle{}, false, err
}
command.Cycle = normalizeCycle(command.Cycle)
command.OperatorAdminID = command.OperatorAdminID
if command.OperatorAdminID <= 0 {
return domain.Cycle{}, false, xerr.New(xerr.InvalidArgument, "operator_admin_id is required")
}

View File

@ -91,6 +91,7 @@ func New(cfg config.Config) (*App, error) {
var userCountryQueryClient client.UserCountryQueryClient = client.NewGRPCUserCountryQueryClient(userConn)
var userHostClient client.UserHostClient = client.NewGRPCUserHostClient(userConn)
var userSocialClient client.UserSocialClient = client.NewGRPCUserSocialClient(userConn)
var userCPClient client.UserCPClient = client.NewGRPCUserCPClient(userConn)
var appRegistryClient client.AppRegistryClient = client.NewGRPCAppRegistryClient(userConn)
var walletClient client.WalletClient = client.NewGRPCWalletClient(walletConn)
var messageClient client.MessageInboxClient = client.NewGRPCMessageInboxClient(activityConn)
@ -158,6 +159,7 @@ func New(cfg config.Config) (*App, error) {
handler.SetUserCountryQueryClient(userCountryQueryClient)
handler.SetUserHostClient(userHostClient)
handler.SetUserSocialClient(userSocialClient)
handler.SetUserCPClient(userCPClient)
handler.SetAppRegistryClient(appRegistryClient)
handler.SetWalletClient(walletClient)
handler.SetMessageInboxClient(messageClient)

View File

@ -17,6 +17,7 @@ type UserAuthClient interface {
RefreshToken(ctx context.Context, req *userv1.RefreshTokenRequest) (*userv1.RefreshTokenResponse, error)
Logout(ctx context.Context, req *userv1.LogoutRequest) (*userv1.LogoutResponse, error)
RecordLoginBlocked(ctx context.Context, req *userv1.RecordLoginBlockedRequest) (*userv1.RecordLoginBlockedResponse, error)
AppHeartbeat(ctx context.Context, req *userv1.AppHeartbeatRequest) (*userv1.AppHeartbeatResponse, error)
}
// UserIdentityClient 抽象 gateway 对 user-service 短号能力的依赖。
@ -53,6 +54,14 @@ type UserSocialClient interface {
SubmitReport(ctx context.Context, req *userv1.SubmitReportRequest) (*userv1.SubmitReportResponse, error)
}
// UserCPClient 抽象 gateway 对 user-service CP/兄弟/姐妹关系能力的依赖。
type UserCPClient interface {
ListCPApplications(ctx context.Context, req *userv1.ListCPApplicationsRequest) (*userv1.ListCPApplicationsResponse, error)
AcceptCPApplication(ctx context.Context, req *userv1.AcceptCPApplicationRequest) (*userv1.AcceptCPApplicationResponse, error)
RejectCPApplication(ctx context.Context, req *userv1.RejectCPApplicationRequest) (*userv1.RejectCPApplicationResponse, error)
ListCPRelationships(ctx context.Context, req *userv1.ListCPRelationshipsRequest) (*userv1.ListCPRelationshipsResponse, error)
}
// UserDeviceClient 抽象 gateway 对 user-service 设备 push token 的依赖。
type UserDeviceClient interface {
BindPushToken(ctx context.Context, req *userv1.BindPushTokenRequest) (*userv1.BindPushTokenResponse, error)
@ -108,6 +117,10 @@ type grpcUserSocialClient struct {
client userv1.UserSocialServiceClient
}
type grpcUserCPClient struct {
client userv1.UserCPServiceClient
}
type grpcUserDeviceClient struct {
client userv1.UserDeviceServiceClient
}
@ -152,6 +165,13 @@ func NewGRPCUserSocialClient(conn *grpc.ClientConn) UserSocialClient {
}
}
// NewGRPCUserCPClient 用 gRPC 连接构造 CP 关系 client。
func NewGRPCUserCPClient(conn *grpc.ClientConn) UserCPClient {
return &grpcUserCPClient{
client: userv1.NewUserCPServiceClient(conn),
}
}
// NewGRPCUserDeviceClient 用 gRPC 连接构造 user-service 设备推送 client。
func NewGRPCUserDeviceClient(conn *grpc.ClientConn) UserDeviceClient {
return &grpcUserDeviceClient{
@ -208,6 +228,10 @@ func (c *grpcUserAuthClient) RecordLoginBlocked(ctx context.Context, req *userv1
return c.client.RecordLoginBlocked(ctx, req)
}
func (c *grpcUserAuthClient) AppHeartbeat(ctx context.Context, req *userv1.AppHeartbeatRequest) (*userv1.AppHeartbeatResponse, error) {
return c.client.AppHeartbeat(ctx, req)
}
func (c *grpcUserIdentityClient) GetUserIdentity(ctx context.Context, req *userv1.GetUserIdentityRequest) (*userv1.GetUserIdentityResponse, error) {
return c.client.GetUserIdentity(ctx, req)
}
@ -296,6 +320,22 @@ func (c *grpcUserSocialClient) SubmitReport(ctx context.Context, req *userv1.Sub
return c.client.SubmitReport(ctx, req)
}
func (c *grpcUserCPClient) ListCPApplications(ctx context.Context, req *userv1.ListCPApplicationsRequest) (*userv1.ListCPApplicationsResponse, error) {
return c.client.ListCPApplications(ctx, req)
}
func (c *grpcUserCPClient) AcceptCPApplication(ctx context.Context, req *userv1.AcceptCPApplicationRequest) (*userv1.AcceptCPApplicationResponse, error) {
return c.client.AcceptCPApplication(ctx, req)
}
func (c *grpcUserCPClient) RejectCPApplication(ctx context.Context, req *userv1.RejectCPApplicationRequest) (*userv1.RejectCPApplicationResponse, error) {
return c.client.RejectCPApplication(ctx, req)
}
func (c *grpcUserCPClient) ListCPRelationships(ctx context.Context, req *userv1.ListCPRelationshipsRequest) (*userv1.ListCPRelationshipsResponse, error) {
return c.client.ListCPRelationships(ctx, req)
}
func (c *grpcUserDeviceClient) BindPushToken(ctx context.Context, req *userv1.BindPushTokenRequest) (*userv1.BindPushTokenResponse, error) {
return c.client.BindPushToken(ctx, req)
}

View File

@ -26,12 +26,14 @@ type ObjectUploader interface {
// It does not own user identity or resource semantics; those remain in their owner services/packages.
type Handler struct {
appConfigReader ConfigReader
userAuthClient client.UserAuthClient
userDeviceClient client.UserDeviceClient
objectUploader ObjectUploader
}
type Config struct {
AppConfigReader ConfigReader
UserAuthClient client.UserAuthClient
UserDeviceClient client.UserDeviceClient
ObjectUploader ObjectUploader
}
@ -39,6 +41,7 @@ type Config struct {
func New(config Config) *Handler {
return &Handler{
appConfigReader: config.AppConfigReader,
userAuthClient: config.UserAuthClient,
userDeviceClient: config.UserDeviceClient,
objectUploader: config.ObjectUploader,
}
@ -79,3 +82,7 @@ func (h *Handler) ProxyEffectMedia(writer http.ResponseWriter, request *http.Req
func (h *Handler) HandlePushToken(writer http.ResponseWriter, request *http.Request) {
h.handlePushToken(writer, request)
}
func (h *Handler) AppHeartbeat(writer http.ResponseWriter, request *http.Request) {
h.appHeartbeat(writer, request)
}

View File

@ -0,0 +1,41 @@
package appapi
import (
"net/http"
userv1 "hyapp.local/api/proto/user/v1"
"hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
)
type appHeartbeatData struct {
Accepted bool `json:"accepted"`
HeartbeatAtMS int64 `json:"heartbeat_at_ms"`
ServerTimeMS int64 `json:"server_time_ms"`
}
// appHeartbeat 刷新当前登录 App 会话,不写房间 presence也不改变麦位在线状态。
func (h *Handler) appHeartbeat(writer http.ResponseWriter, request *http.Request) {
if h.userAuthClient == nil {
// App 心跳必须落 user-service session未注入时不能返回假在线。
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
sessionID := auth.SessionIDFromContext(request.Context())
resp, err := h.userAuthClient.AppHeartbeat(request.Context(), &userv1.AppHeartbeatRequest{
Meta: httpkit.UserMeta(request, httpkit.FirstHeader(request, "X-Device-ID", "X-HY-Device-ID")),
UserId: auth.UserIDFromContext(request.Context()),
SessionId: sessionID,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, appHeartbeatData{
Accepted: resp.GetAccepted(),
HeartbeatAtMS: resp.GetHeartbeatAtMs(),
ServerTimeMS: resp.GetServerTimeMs(),
})
}

View File

@ -29,6 +29,7 @@ type Handler struct {
userIdentityClient client.UserIdentityClient
userProfileClient client.UserProfileClient
userSocialClient client.UserSocialClient
userCPClient client.UserCPClient
userDeviceClient client.UserDeviceClient
userCountryClient client.UserCountryQueryClient
userHostClient client.UserHostClient
@ -175,6 +176,11 @@ func (h *Handler) SetUserSocialClient(userSocialClient client.UserSocialClient)
h.userSocialClient = userSocialClient
}
// SetUserCPClient 注入 user-service CP/兄弟/姐妹关系 client。
func (h *Handler) SetUserCPClient(userCPClient client.UserCPClient) {
h.userCPClient = userCPClient
}
// SetAppRegistryClient 注入 App 注册表解析 clientgateway 用它把包名转换成内部 app_code。
func (h *Handler) SetAppRegistryClient(appRegistryClient client.AppRegistryClient) {
h.appRegistryClient = appRegistryClient

View File

@ -59,6 +59,7 @@ type AppHandlers struct {
UploadAvatar http.HandlerFunc
ProxyEffectMedia http.HandlerFunc
HandlePushToken http.HandlerFunc
AppHeartbeat http.HandlerFunc
}
type UserHandlers struct {
@ -97,6 +98,10 @@ type UserHandlers struct {
ListMyFollowing http.HandlerFunc
ListMyFriends http.HandlerFunc
ListMyFriendApplications http.HandlerFunc
ListCPApplications http.HandlerFunc
AcceptCPApplication http.HandlerFunc
RejectCPApplication http.HandlerFunc
ListCPRelationships http.HandlerFunc
GetMyProfile http.HandlerFunc
GetMyAppearance http.HandlerFunc
GetUserAppearance http.HandlerFunc
@ -306,6 +311,7 @@ func (r routes) registerAppRoutes() {
r.auth("/files/upload", "", h.UploadFile)
r.auth("/files/avatar/upload", "", h.UploadAvatar)
r.public("/media/effects/proxy", http.MethodGet, h.ProxyEffectMedia)
r.auth("/app/heartbeat", http.MethodPost, h.AppHeartbeat)
r.profile("/devices/push-token", "", h.HandlePushToken)
}
@ -346,6 +352,10 @@ func (r routes) registerUserRoutes() {
r.profile("/users/me/following", http.MethodGet, h.ListMyFollowing)
r.profile("/users/me/friends", http.MethodGet, h.ListMyFriends)
r.profile("/users/me/friend-requests", http.MethodGet, h.ListMyFriendApplications)
r.profile("/cp/applications", http.MethodGet, h.ListCPApplications)
r.profile("/cp/applications/{application_id}/accept", http.MethodPost, h.AcceptCPApplication)
r.profile("/cp/applications/{application_id}/reject", http.MethodPost, h.RejectCPApplication)
r.profile("/cp/relationships", http.MethodGet, h.ListCPRelationships)
r.profile("/appearance", http.MethodGet, h.GetMyAppearance)
r.profile("/users/me/appearance", http.MethodGet, h.GetMyAppearance)
r.profile("/users/me", "", h.GetMyProfile)

View File

@ -293,6 +293,7 @@ type fakeUserAuthClient struct {
lastRefresh *userv1.RefreshTokenRequest
lastLogout *userv1.LogoutRequest
lastBlocked *userv1.RecordLoginBlockedRequest
lastAppHeartbeat *userv1.AppHeartbeatRequest
loginPasswordCount int
loginThirdCount int
refreshCount int
@ -625,6 +626,11 @@ func (f *fakeUserAuthClient) RecordLoginBlocked(_ context.Context, req *userv1.R
return &userv1.RecordLoginBlockedResponse{Recorded: true}, nil
}
func (f *fakeUserAuthClient) AppHeartbeat(_ context.Context, req *userv1.AppHeartbeatRequest) (*userv1.AppHeartbeatResponse, error) {
f.lastAppHeartbeat = req
return &userv1.AppHeartbeatResponse{Accepted: true, HeartbeatAtMs: 1_778_000_005_000, ServerTimeMs: 1_778_000_005_000}, nil
}
func (f *fakeUserProfileClient) GetUser(_ context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
f.lastGet = req
f.getRequests = append(f.getRequests, req)
@ -654,6 +660,9 @@ func (f *fakeUserProfileClient) GetUser(_ context.Context, req *userv1.GetUserRe
Gender: "female",
Avatar: "https://cdn.example/avatar.png",
Birth: "2000-01-02",
CountryId: 840,
Country: "US",
CountryName: "United States",
RegionId: regionID,
ProfileCompleted: true,
OnboardingStatus: "completed",
@ -2573,6 +2582,37 @@ func TestBindPushTokenUsesAuthenticatedUserAndDeviceMeta(t *testing.T) {
}
}
func TestAppHeartbeatUsesAuthenticatedSession(t *testing.T) {
userClient := &fakeUserAuthClient{}
handler := NewHandlerWithClients(&fakeRoomClient{}, userClient, nil, &fakeUserProfileClient{})
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodPost, "/api/v1/app/heartbeat", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
request.Header.Set("X-Device-ID", "dev-heartbeat")
request.Header.Set("X-Request-ID", "req-app-heartbeat")
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 userClient.lastAppHeartbeat == nil || userClient.lastAppHeartbeat.GetUserId() != 42 || userClient.lastAppHeartbeat.GetSessionId() != "sess-test" {
t.Fatalf("app heartbeat must use authenticated user and session: %+v", userClient.lastAppHeartbeat)
}
if userClient.lastAppHeartbeat.GetMeta().GetDeviceId() != "dev-heartbeat" {
t.Fatalf("app heartbeat must pass device meta: %+v", userClient.lastAppHeartbeat.GetMeta())
}
var response httpkit.ResponseEnvelope
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode app heartbeat response failed: %v", err)
}
data, ok := response.Data.(map[string]any)
if !ok || data["accepted"] != true || data["heartbeat_at_ms"].(float64) != 1_778_000_005_000 {
t.Fatalf("app heartbeat response mismatch: %+v", response.Data)
}
}
func TestDeletePushTokenUsesAuthenticatedUser(t *testing.T) {
deviceClient := &fakeUserDeviceClient{}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
@ -6423,7 +6463,7 @@ func TestJoinRoomClosedWritesRoomClosedEnvelope(t *testing.T) {
roomClient := &fakeRoomClient{
joinErr: xerr.ToGRPCError(xerr.New(xerr.RoomClosed, "room closed")),
}
router := NewHandler(roomClient).Routes(auth.NewVerifier("secret"))
router := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}).Routes(auth.NewVerifier("secret"))
body := []byte(`{"room_id":"room-closed","command_id":"cmd-join-closed"}`)
request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/join", bytes.NewReader(body))
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))

View File

@ -35,6 +35,7 @@ type giftConfigData struct {
PresentationJSON string `json:"presentation_json"`
PriceVersion string `json:"price_version"`
GiftTypeCode string `json:"gift_type_code"`
CPRelationType string `json:"cp_relation_type,omitempty"`
ChargeAssetType string `json:"charge_asset_type"`
CoinPrice int64 `json:"coin_price"`
GiftPointAmount int64 `json:"gift_point_amount"`
@ -69,6 +70,7 @@ func giftFromProto(gift *walletv1.GiftConfig) giftConfigData {
PresentationJSON: gift.GetPresentationJson(),
PriceVersion: gift.GetPriceVersion(),
GiftTypeCode: gift.GetGiftTypeCode(),
CPRelationType: gift.GetCpRelationType(),
ChargeAssetType: gift.GetChargeAssetType(),
CoinPrice: gift.GetCoinPrice(),
GiftPointAmount: gift.GetGiftPointAmount(),

View File

@ -53,6 +53,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
UserIdentityClient: h.userIdentityClient,
UserProfileClient: h.userProfileClient,
UserSocialClient: h.userSocialClient,
UserCPClient: h.userCPClient,
UserCountryClient: h.userCountryClient,
UserHostClient: h.userHostClient,
WalletClient: h.walletClient,
@ -94,6 +95,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
})
appAPI := appapi.New(appapi.Config{
AppConfigReader: h.appConfigReader,
UserAuthClient: h.userClient,
UserDeviceClient: h.userDeviceClient,
ObjectUploader: h.objectUploader,
})
@ -161,6 +163,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
UploadAvatar: appAPI.UploadAvatar,
ProxyEffectMedia: appAPI.ProxyEffectMedia,
HandlePushToken: appAPI.HandlePushToken,
AppHeartbeat: appAPI.AppHeartbeat,
},
User: userHandlers,
Manager: managerAPI.ManagerHandlers(),

View File

@ -0,0 +1,242 @@
package userapi
import (
"net/http"
"strings"
userv1 "hyapp.local/api/proto/user/v1"
"hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
)
type cpUserProfileData struct {
UserID string `json:"user_id"`
DisplayUserID string `json:"display_user_id"`
Username string `json:"username"`
Avatar string `json:"avatar"`
}
type cpGiftSnapshotData struct {
GiftID string `json:"gift_id"`
GiftName string `json:"gift_name"`
GiftIconURL string `json:"gift_icon_url"`
GiftAnimationURL string `json:"gift_animation_url"`
GiftCount int32 `json:"gift_count"`
GiftValue int64 `json:"gift_value"`
BillingReceiptID string `json:"billing_receipt_id"`
}
type cpApplicationData struct {
ApplicationID string `json:"application_id"`
RelationType string `json:"relation_type"`
Status string `json:"status"`
Requester *cpUserProfileData `json:"requester,omitempty"`
Target *cpUserProfileData `json:"target,omitempty"`
RoomID string `json:"room_id"`
RoomRegionID int64 `json:"room_region_id"`
Gift *cpGiftSnapshotData `json:"gift,omitempty"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
ExpiresAtMS int64 `json:"expires_at_ms"`
DecidedAtMS int64 `json:"decided_at_ms"`
}
type cpRelationshipData struct {
RelationshipID string `json:"relationship_id"`
RelationType string `json:"relation_type"`
Status string `json:"status"`
Me *cpUserProfileData `json:"me,omitempty"`
Partner *cpUserProfileData `json:"partner,omitempty"`
IntimacyValue int64 `json:"intimacy_value"`
Level int32 `json:"level"`
FormedAtMS int64 `json:"formed_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type cpRejectRequest struct {
Reason string `json:"reason"`
}
// listCPApplications 返回当前用户收到或发出的 CP/兄弟/姐妹申请。
func (h *Handler) listCPApplications(writer http.ResponseWriter, request *http.Request) {
if h.userCPClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
page, pageSize, ok := socialPage(request)
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.userCPClient.ListCPApplications(request.Context(), &userv1.ListCPApplicationsRequest{
Meta: httpkit.UserMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
Direction: strings.TrimSpace(request.URL.Query().Get("direction")),
Status: strings.TrimSpace(request.URL.Query().Get("status")),
Page: page,
PageSize: pageSize,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
applications := make([]cpApplicationData, 0, len(resp.GetApplications()))
for _, item := range resp.GetApplications() {
applications = append(applications, cpApplicationFromProto(item))
}
httpkit.WriteOK(writer, request, map[string]any{
"applications": applications,
"total": resp.GetTotal(),
"server_time_ms": resp.GetServerTimeMs(),
})
}
// acceptCPApplication 同意一条申请;同意后同一对用户其他 pending 类型由 user-service 阻断。
func (h *Handler) acceptCPApplication(writer http.ResponseWriter, request *http.Request) {
if h.userCPClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
applicationID := strings.TrimSpace(request.PathValue("application_id"))
if applicationID == "" {
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
return
}
resp, err := h.userCPClient.AcceptCPApplication(request.Context(), &userv1.AcceptCPApplicationRequest{
Meta: httpkit.UserMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
ApplicationId: applicationID,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, map[string]any{
"application": cpApplicationFromProto(resp.GetApplication()),
"relationship": cpRelationshipFromProto(resp.GetRelationship()),
})
}
// rejectCPApplication 拒绝一条申请;拒绝不会删除历史申请,客户端仍可看到 decided 状态。
func (h *Handler) rejectCPApplication(writer http.ResponseWriter, request *http.Request) {
if h.userCPClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
applicationID := strings.TrimSpace(request.PathValue("application_id"))
if applicationID == "" {
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
return
}
var body cpRejectRequest
if request.Body != nil && request.ContentLength != 0 && !httpkit.Decode(writer, request, &body) {
return
}
resp, err := h.userCPClient.RejectCPApplication(request.Context(), &userv1.RejectCPApplicationRequest{
Meta: httpkit.UserMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
ApplicationId: applicationID,
Reason: body.Reason,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, map[string]any{"application": cpApplicationFromProto(resp.GetApplication())})
}
// listCPRelationships 返回当前用户 active 关系;服务端固定限制一个用户只能同时存在一条关系。
func (h *Handler) listCPRelationships(writer http.ResponseWriter, request *http.Request) {
if h.userCPClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
page, pageSize, ok := socialPage(request)
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.userCPClient.ListCPRelationships(request.Context(), &userv1.ListCPRelationshipsRequest{
Meta: httpkit.UserMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
RelationType: strings.TrimSpace(request.URL.Query().Get("relation_type")),
Page: page,
PageSize: pageSize,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
relationships := make([]cpRelationshipData, 0, len(resp.GetRelationships()))
for _, item := range resp.GetRelationships() {
relationships = append(relationships, cpRelationshipFromProto(item))
}
httpkit.WriteOK(writer, request, map[string]any{
"relationships": relationships,
"total": resp.GetTotal(),
})
}
func cpApplicationFromProto(item *userv1.CPApplication) cpApplicationData {
if item == nil {
return cpApplicationData{}
}
return cpApplicationData{
ApplicationID: item.GetApplicationId(),
RelationType: item.GetRelationType(),
Status: item.GetStatus(),
Requester: cpUserProfileFromProto(item.GetRequester()),
Target: cpUserProfileFromProto(item.GetTarget()),
RoomID: item.GetRoomId(),
RoomRegionID: item.GetRoomRegionId(),
Gift: cpGiftSnapshotFromProto(item.GetGift()),
CreatedAtMS: item.GetCreatedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
ExpiresAtMS: item.GetExpiresAtMs(),
DecidedAtMS: item.GetDecidedAtMs(),
}
}
func cpRelationshipFromProto(item *userv1.CPRelationship) cpRelationshipData {
if item == nil {
return cpRelationshipData{}
}
return cpRelationshipData{
RelationshipID: item.GetRelationshipId(),
RelationType: item.GetRelationType(),
Status: item.GetStatus(),
Me: cpUserProfileFromProto(item.GetMe()),
Partner: cpUserProfileFromProto(item.GetPartner()),
IntimacyValue: item.GetIntimacyValue(),
Level: item.GetLevel(),
FormedAtMS: item.GetFormedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
}
}
func cpUserProfileFromProto(item *userv1.CPUserProfile) *cpUserProfileData {
if item == nil || item.GetUserId() <= 0 {
return nil
}
return &cpUserProfileData{
UserID: userIDString(item.GetUserId()),
DisplayUserID: item.GetDisplayUserId(),
Username: item.GetUsername(),
Avatar: item.GetAvatar(),
}
}
func cpGiftSnapshotFromProto(item *userv1.CPGiftSnapshot) *cpGiftSnapshotData {
if item == nil {
return nil
}
return &cpGiftSnapshotData{
GiftID: item.GetGiftId(),
GiftName: item.GetGiftName(),
GiftIconURL: item.GetGiftIconUrl(),
GiftAnimationURL: item.GetGiftAnimationUrl(),
GiftCount: item.GetGiftCount(),
GiftValue: item.GetGiftValue(),
BillingReceiptID: item.GetBillingReceiptId(),
}
}

View File

@ -13,6 +13,7 @@ type Handler struct {
userIdentityClient client.UserIdentityClient
userProfileClient client.UserProfileClient
userSocialClient client.UserSocialClient
userCPClient client.UserCPClient
userCountryClient client.UserCountryQueryClient
userHostClient client.UserHostClient
walletClient client.WalletClient
@ -25,6 +26,7 @@ type Config struct {
UserIdentityClient client.UserIdentityClient
UserProfileClient client.UserProfileClient
UserSocialClient client.UserSocialClient
UserCPClient client.UserCPClient
UserCountryClient client.UserCountryQueryClient
UserHostClient client.UserHostClient
WalletClient client.WalletClient
@ -38,6 +40,7 @@ func New(config Config) *Handler {
userIdentityClient: config.UserIdentityClient,
userProfileClient: config.UserProfileClient,
userSocialClient: config.UserSocialClient,
userCPClient: config.UserCPClient,
userCountryClient: config.UserCountryClient,
userHostClient: config.UserHostClient,
walletClient: config.WalletClient,
@ -81,6 +84,10 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
ListMyFollowing: h.listMyFollowing,
ListMyFriends: h.listMyFriends,
ListMyFriendApplications: h.listMyFriendApplications,
ListCPApplications: h.listCPApplications,
AcceptCPApplication: h.acceptCPApplication,
RejectCPApplication: h.rejectCPApplication,
ListCPRelationships: h.listCPRelationships,
GetMyProfile: h.getMyProfile,
GetMyAppearance: h.getMyAppearance,
GetUserAppearance: h.getUserAppearance,

View File

@ -16,6 +16,7 @@ tencent_im:
admin_user_sig_ttl: 24h
endpoint: "adminapisgp.im.qcloud.com"
request_timeout: 5s
group_id_prefix: "test_"
wallet_notice_worker:
enabled: false
@ -37,10 +38,20 @@ room_notice_worker:
initial_backoff: 5s
max_backoff: 5m
rocketmq:
# Docker 本地默认关闭;接入外部 RocketMQ 后只消费 owner outbox 私有通知事实。
cp_notice_worker:
enabled: false
name_servers: []
poll_interval: 1s
batch_size: 100
lock_ttl: 30s
publish_timeout: 3s
max_retry_count: 10
initial_backoff: 5s
max_backoff: 5m
rocketmq:
# Docker 本地真实链路消费 owner outbox MQ fanout投递位点仍写 notice_delivery_events。
enabled: true
name_servers: ["rocketmq-namesrv:9876"]
name_server_domain: ""
access_key: ""
secret_key: ""
@ -55,6 +66,11 @@ rocketmq:
topic: "hyapp_room_outbox"
consumer_group: "hyapp-notice-room-outbox"
consumer_max_reconsume_times: 16
user_outbox:
enabled: true
topic: "hyapp_user_outbox"
consumer_group: "hyapp-notice-cp-user-outbox"
consumer_max_reconsume_times: 16
log:
level: info

View File

@ -16,6 +16,7 @@ tencent_im:
admin_user_sig_ttl: 24h
endpoint: "console.tim.qq.com"
request_timeout: 5s
group_id_prefix: ""
wallet_notice_worker:
enabled: false
@ -37,6 +38,16 @@ room_notice_worker:
initial_backoff: 5s
max_backoff: 5m
cp_notice_worker:
enabled: false
poll_interval: 1s
batch_size: 100
lock_ttl: 30s
publish_timeout: 3s
max_retry_count: 10
initial_backoff: 5s
max_backoff: 5m
rocketmq:
enabled: true
name_servers:
@ -55,6 +66,11 @@ rocketmq:
topic: "hyapp_room_outbox"
consumer_group: "hyapp-notice-room-outbox"
consumer_max_reconsume_times: 16
user_outbox:
enabled: true
topic: "hyapp_user_outbox"
consumer_group: "hyapp-notice-cp-user-outbox"
consumer_max_reconsume_times: 16
log:
level: info

View File

@ -16,6 +16,7 @@ tencent_im:
admin_user_sig_ttl: 24h
endpoint: "adminapisgp.im.qcloud.com"
request_timeout: 5s
group_id_prefix: ""
wallet_notice_worker:
enabled: false
@ -37,6 +38,16 @@ room_notice_worker:
initial_backoff: 5s
max_backoff: 5m
cp_notice_worker:
enabled: false
poll_interval: 1s
batch_size: 100
lock_ttl: 30s
publish_timeout: 3s
max_retry_count: 10
initial_backoff: 5s
max_backoff: 5m
rocketmq:
# 本地默认关闭;开启后 notice 只消费 owner outbox MQ fanout仍写 notice_delivery_events 做幂等。
enabled: false
@ -55,6 +66,11 @@ rocketmq:
topic: "hyapp_room_outbox"
consumer_group: "hyapp-notice-room-outbox"
consumer_max_reconsume_times: 16
user_outbox:
enabled: false
topic: "hyapp_user_outbox"
consumer_group: "hyapp-notice-cp-user-outbox"
consumer_max_reconsume_times: 16
log:
level: info

View File

@ -19,8 +19,10 @@ import (
"hyapp/pkg/rocketmqx"
"hyapp/pkg/roommq"
"hyapp/pkg/tencentim"
"hyapp/pkg/usermq"
"hyapp/pkg/walletmq"
"hyapp/services/notice-service/internal/config"
"hyapp/services/notice-service/internal/modules/cpnotice"
"hyapp/services/notice-service/internal/modules/roomnotice"
"hyapp/services/notice-service/internal/modules/walletnotice"
mysqlplatform "hyapp/services/notice-service/internal/platform/mysql"
@ -39,6 +41,8 @@ type App struct {
roomNoticeService *roomnotice.Service
roomWorkerOptions roomnotice.RoomNoticeWorkerOptions
roomWorkerEnabled bool
cpNoticeService *cpnotice.Service
cpWorkerOptions cpnotice.CPNoticeWorkerOptions
mqConsumers []*rocketmqx.Consumer
workerCtx context.Context
workerCancel context.CancelFunc
@ -73,7 +77,8 @@ func New(cfg config.Config) (*App, error) {
_ = store.Close()
return nil, err
}
var publisher walletnotice.UserMessagePublisher
cpRepo := cpnotice.NewMySQLRepository(store.DB)
var publisher *tencentim.RESTClient
if cfg.TencentIM.Enabled {
tencentClient, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig())
if err != nil {
@ -103,6 +108,11 @@ func New(cfg config.Config) (*App, error) {
_ = store.Close()
return nil, errors.New("room outbox mq consumer requires tencent_im.enabled")
}
if cfg.RocketMQ.UserOutbox.Enabled && publisher == nil {
_ = listener.Close()
_ = store.Close()
return nil, errors.New("user outbox mq consumer requires tencent_im.enabled")
}
server := grpc.NewServer(grpc.UnaryInterceptor(logx.UnaryServerInterceptor("notice-service")))
health := grpchealth.NewServingChecker("notice-service", grpchealth.Dependency{
@ -120,7 +130,9 @@ func New(cfg config.Config) (*App, error) {
walletSvc := walletnotice.New(walletnotice.Config{NodeID: cfg.NodeID}, walletRepo, publisher)
roomSvc := roomnotice.New(roomnotice.Config{NodeID: cfg.NodeID}, roomRepo, publisher)
roomOptions := roomNoticeWorkerOptions(cfg.RoomNoticeWorker)
mqConsumers := make([]*rocketmqx.Consumer, 0, 2)
cpSvc := cpnotice.New(cpnotice.Config{NodeID: cfg.NodeID, GroupIDPrefix: cfg.TencentIM.GroupIDPrefix}, cpRepo, publisher)
cpOptions := cpNoticeWorkerOptions(cfg.CPNoticeWorker)
mqConsumers := make([]*rocketmqx.Consumer, 0, 3)
if cfg.RocketMQ.WalletOutbox.Enabled {
consumer, err := rocketmqx.NewConsumer(rocketMQConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.ConsumerGroup, cfg.RocketMQ.WalletOutbox.ConsumerMaxReconsumeTimes))
if err != nil {
@ -166,6 +178,29 @@ func New(cfg config.Config) (*App, error) {
}
mqConsumers = append(mqConsumers, consumer)
}
if cfg.RocketMQ.UserOutbox.Enabled {
consumer, err := rocketmqx.NewConsumer(rocketMQConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.UserOutbox.ConsumerGroup, cfg.RocketMQ.UserOutbox.ConsumerMaxReconsumeTimes))
if err != nil {
shutdownConsumers(mqConsumers)
_ = listener.Close()
_ = store.Close()
return nil, err
}
if err := consumer.Subscribe(cfg.RocketMQ.UserOutbox.Topic, usermq.TagUserOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
userMessage, err := usermq.DecodeUserOutboxMessage(message.Body)
if err != nil {
return err
}
_, err = cpSvc.ProcessUserOutboxMessage(appcode.WithContext(ctx, userMessage.AppCode), userMessage, cpOptions)
return err
}); err != nil {
_ = listener.Close()
_ = store.Close()
shutdownConsumers(mqConsumers)
return nil, err
}
mqConsumers = append(mqConsumers, consumer)
}
workerCtx, workerCancel := context.WithCancel(context.Background())
return &App{
server: server,
@ -179,6 +214,8 @@ func New(cfg config.Config) (*App, error) {
roomNoticeService: roomSvc,
roomWorkerOptions: roomOptions,
roomWorkerEnabled: cfg.RoomNoticeWorker.Enabled,
cpNoticeService: cpSvc,
cpWorkerOptions: cpOptions,
mqConsumers: mqConsumers,
workerCtx: workerCtx,
workerCancel: workerCancel,
@ -279,6 +316,17 @@ func roomNoticeWorkerOptions(cfg config.WalletNoticeWorkerConfig) roomnotice.Roo
}
}
func cpNoticeWorkerOptions(cfg config.WalletNoticeWorkerConfig) cpnotice.CPNoticeWorkerOptions {
return cpnotice.CPNoticeWorkerOptions{
BatchSize: cfg.BatchSize,
LockTTL: cfg.LockTTL,
PublishTimeout: cfg.PublishTimeout,
MaxRetryCount: cfg.MaxRetryCount,
InitialBackoff: cfg.InitialBackoff,
MaxBackoff: cfg.MaxBackoff,
}
}
func (a *App) startMQ() error {
for _, consumer := range a.mqConsumers {
if err := consumer.Start(); err != nil {

View File

@ -31,6 +31,8 @@ type Config struct {
WalletNoticeWorker WalletNoticeWorkerConfig `yaml:"wallet_notice_worker"`
// RoomNoticeWorker 消费 room_outbox 中需要给单个用户发送的房间通知。
RoomNoticeWorker WalletNoticeWorkerConfig `yaml:"room_notice_worker"`
// CPNoticeWorker 控制 CP user_outbox 事实转腾讯 IM 的投递锁和重试策略。
CPNoticeWorker WalletNoticeWorkerConfig `yaml:"cp_notice_worker"`
// RocketMQ 消费 owner service 发布的 outbox fanout topic。
RocketMQ RocketMQConfig `yaml:"rocketmq"`
MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"`
@ -46,6 +48,7 @@ type TencentIMConfig struct {
AdminUserSigTTL time.Duration `yaml:"admin_user_sig_ttl"`
Endpoint string `yaml:"endpoint"`
RequestTimeout time.Duration `yaml:"request_timeout"`
GroupIDPrefix string `yaml:"group_id_prefix"`
}
// RESTConfig converts YAML config into shared Tencent IM REST config.
@ -88,6 +91,7 @@ type RocketMQConfig struct {
VIPChannel bool `yaml:"vip_channel"`
WalletOutbox RoomOutboxMQConfig `yaml:"wallet_outbox"`
RoomOutbox RoomOutboxMQConfig `yaml:"room_outbox"`
UserOutbox RoomOutboxMQConfig `yaml:"user_outbox"`
}
// RoomOutboxMQConfig 控制 notice 对 room_outbox topic 的消费位点。
@ -137,6 +141,16 @@ func Default() Config {
InitialBackoff: 5 * time.Second,
MaxBackoff: 5 * time.Minute,
},
CPNoticeWorker: WalletNoticeWorkerConfig{
Enabled: false,
PollInterval: time.Second,
BatchSize: 100,
LockTTL: 30 * time.Second,
PublishTimeout: 3 * time.Second,
MaxRetryCount: 10,
InitialBackoff: 5 * time.Second,
MaxBackoff: 5 * time.Minute,
},
RocketMQ: defaultRocketMQConfig(),
Log: logx.Config{
Level: "info",
@ -161,6 +175,12 @@ func defaultRocketMQConfig() RocketMQConfig {
ConsumerGroup: "hyapp-notice-room-outbox",
ConsumerMaxReconsumeTimes: 16,
},
UserOutbox: RoomOutboxMQConfig{
Enabled: false,
Topic: "hyapp_user_outbox",
ConsumerGroup: "hyapp-notice-cp-user-outbox",
ConsumerMaxReconsumeTimes: 16,
},
}
}
@ -203,6 +223,7 @@ func Load(path string) (Config, error) {
}
cfg.WalletNoticeWorker = normalizeWalletNoticeWorker(cfg.WalletNoticeWorker)
cfg.RoomNoticeWorker = normalizeWalletNoticeWorker(cfg.RoomNoticeWorker)
cfg.CPNoticeWorker = normalizeWalletNoticeWorker(cfg.CPNoticeWorker)
rocketMQ, err := normalizeRocketMQConfig(cfg.RocketMQ)
if err != nil {
return Config{}, err
@ -214,6 +235,9 @@ func Load(path string) (Config, error) {
if cfg.RoomNoticeWorker.Enabled {
return Config{}, errors.New("room_notice_worker direct room_outbox polling is disabled; enable rocketmq.room_outbox")
}
if cfg.CPNoticeWorker.Enabled {
return Config{}, errors.New("cp_notice_worker direct user_outbox polling is disabled; enable rocketmq.user_outbox")
}
if strings.TrimSpace(cfg.TencentIM.AdminIdentifier) == "" {
cfg.TencentIM.AdminIdentifier = "administrator"
}
@ -223,6 +247,7 @@ func Load(path string) (Config, error) {
if strings.TrimSpace(cfg.TencentIM.Endpoint) == "" {
cfg.TencentIM.Endpoint = tencentim.DefaultEndpoint
}
cfg.TencentIM.GroupIDPrefix = strings.TrimSpace(cfg.TencentIM.GroupIDPrefix)
if cfg.TencentIM.RequestTimeout <= 0 {
cfg.TencentIM.RequestTimeout = 5 * time.Second
}
@ -255,7 +280,16 @@ func normalizeRocketMQConfig(cfg RocketMQConfig) (RocketMQConfig, error) {
if cfg.WalletOutbox.ConsumerMaxReconsumeTimes <= 0 {
cfg.WalletOutbox.ConsumerMaxReconsumeTimes = defaults.WalletOutbox.ConsumerMaxReconsumeTimes
}
if cfg.RoomOutbox.Enabled || cfg.WalletOutbox.Enabled {
if cfg.UserOutbox.Topic = strings.TrimSpace(cfg.UserOutbox.Topic); cfg.UserOutbox.Topic == "" {
cfg.UserOutbox.Topic = defaults.UserOutbox.Topic
}
if cfg.UserOutbox.ConsumerGroup = strings.TrimSpace(cfg.UserOutbox.ConsumerGroup); cfg.UserOutbox.ConsumerGroup == "" {
cfg.UserOutbox.ConsumerGroup = defaults.UserOutbox.ConsumerGroup
}
if cfg.UserOutbox.ConsumerMaxReconsumeTimes <= 0 {
cfg.UserOutbox.ConsumerMaxReconsumeTimes = defaults.UserOutbox.ConsumerMaxReconsumeTimes
}
if cfg.RoomOutbox.Enabled || cfg.WalletOutbox.Enabled || cfg.UserOutbox.Enabled {
cfg.Enabled = true
}
if cfg.Enabled {

View File

@ -0,0 +1,210 @@
package cpnotice
import (
"context"
"database/sql"
"errors"
"strings"
"time"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
)
const (
sourceUserOutbox = "user_outbox"
deliveryStatusDelivering = "delivering"
deliveryStatusDelivered = "delivered"
deliveryStatusRetryable = "retryable"
deliveryStatusFailed = "failed"
)
// CPDelivery is one concrete IM side effect derived from a CP user_outbox fact.
type CPDelivery struct {
AppCode string
EventID string
EventType string
Channel string
NoticeType string
SenderUserID int64
TargetUserID int64
GroupID string
PayloadJSON []byte
RetryCount int
CreatedAtMS int64
}
// MySQLRepository stores CP delivery state in notice_delivery_events only; it never mutates user_outbox.
type MySQLRepository struct {
db *sql.DB
}
// NewMySQLRepository creates CP notice repository.
func NewMySQLRepository(db *sql.DB) *MySQLRepository {
return &MySQLRepository{db: db}
}
// ClaimCPDelivery claims one per-channel CP delivery row. Delivered rows are acknowledged as already handled.
func (r *MySQLRepository) ClaimCPDelivery(ctx context.Context, workerID string, delivery CPDelivery, lockTTL time.Duration) (CPDelivery, bool, error) {
if r == nil || r.db == nil {
return CPDelivery{}, false, xerr.New(xerr.Unavailable, "notice repository is not configured")
}
workerID = strings.TrimSpace(workerID)
if workerID == "" {
return CPDelivery{}, false, xerr.New(xerr.InvalidArgument, "worker_id is required")
}
if lockTTL <= 0 {
lockTTL = 30 * time.Second
}
if err := validateDelivery(delivery); err != nil {
return CPDelivery{}, false, err
}
delivery.AppCode = appcode.Normalize(delivery.AppCode)
now := time.Now()
lockUntilMS := now.Add(lockTTL).UnixMilli()
nowMS := now.UnixMilli()
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return CPDelivery{}, false, err
}
defer func() { _ = tx.Rollback() }()
status, retryCount, lockUntil, nextRetry, err := r.lockDeliveryRow(ctx, tx, delivery)
if errors.Is(err, sql.ErrNoRows) {
if _, err := tx.ExecContext(ctx, `
INSERT INTO notice_delivery_events (
source_name, app_code, source_event_id, channel, notice_type, target_user_id,
status, retry_count, locked_by, lock_until_ms, next_retry_at_ms,
payload_json, last_error, delivered_at_ms, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, 0, CAST(? AS JSON), NULL, 0, ?, ?)`,
sourceUserOutbox, delivery.AppCode, delivery.EventID, delivery.Channel, delivery.NoticeType, delivery.TargetUserID,
deliveryStatusDelivering, workerID, lockUntilMS, string(delivery.PayloadJSON), nowMS, nowMS,
); err != nil {
return CPDelivery{}, false, err
}
delivery.RetryCount = 0
if err := tx.Commit(); err != nil {
return CPDelivery{}, false, err
}
return delivery, true, nil
}
if err != nil {
return CPDelivery{}, false, err
}
if status == deliveryStatusDelivered || status == deliveryStatusFailed {
return CPDelivery{}, false, tx.Commit()
}
if status == deliveryStatusDelivering && lockUntil > nowMS {
return CPDelivery{}, false, tx.Commit()
}
if status == deliveryStatusRetryable && nextRetry > nowMS {
return CPDelivery{}, false, tx.Commit()
}
if _, err := tx.ExecContext(ctx, `
UPDATE notice_delivery_events
SET status = ?, notice_type = ?, target_user_id = ?, retry_count = ?, locked_by = ?,
lock_until_ms = ?, next_retry_at_ms = 0, payload_json = CAST(? AS JSON),
last_error = NULL, updated_at_ms = ?
WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?`,
deliveryStatusDelivering, delivery.NoticeType, delivery.TargetUserID, retryCount, workerID,
lockUntilMS, string(delivery.PayloadJSON), nowMS,
sourceUserOutbox, delivery.AppCode, delivery.EventID, delivery.Channel,
); err != nil {
return CPDelivery{}, false, err
}
delivery.RetryCount = retryCount
if err := tx.Commit(); err != nil {
return CPDelivery{}, false, err
}
return delivery, true, nil
}
func (r *MySQLRepository) lockDeliveryRow(ctx context.Context, tx *sql.Tx, delivery CPDelivery) (string, int, int64, int64, error) {
var status string
var retryCount int
var lockUntil int64
var nextRetry int64
err := tx.QueryRowContext(ctx, `
SELECT status, retry_count, lock_until_ms, next_retry_at_ms
FROM notice_delivery_events
WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?
FOR UPDATE`,
sourceUserOutbox, delivery.AppCode, delivery.EventID, delivery.Channel,
).Scan(&status, &retryCount, &lockUntil, &nextRetry)
return status, retryCount, lockUntil, nextRetry, err
}
// MarkCPDelivered records a successful Tencent IM response for the specific channel.
func (r *MySQLRepository) MarkCPDelivered(ctx context.Context, delivery CPDelivery, deliveredPayload []byte, nowMs int64) error {
if r == nil || r.db == nil {
return xerr.New(xerr.Unavailable, "notice repository is not configured")
}
_, err := r.db.ExecContext(ctx, `
UPDATE notice_delivery_events
SET status = ?, payload_json = CAST(? AS JSON), last_error = NULL,
delivered_at_ms = ?, locked_by = '', lock_until_ms = 0, next_retry_at_ms = 0, updated_at_ms = ?
WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?`,
deliveryStatusDelivered, string(deliveredPayload), nowMs, nowMs,
sourceUserOutbox, delivery.AppCode, delivery.EventID, delivery.Channel,
)
return err
}
// MarkCPRetryable releases the current lock and schedules a channel-level retry.
func (r *MySQLRepository) MarkCPRetryable(ctx context.Context, delivery CPDelivery, retryCount int, nextRetryAtMS int64, lastErr string, nowMs int64) error {
if r == nil || r.db == nil {
return xerr.New(xerr.Unavailable, "notice repository is not configured")
}
_, err := r.db.ExecContext(ctx, `
UPDATE notice_delivery_events
SET status = ?, retry_count = ?, locked_by = '', lock_until_ms = 0,
next_retry_at_ms = ?, last_error = ?, updated_at_ms = ?
WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?`,
deliveryStatusRetryable, retryCount, nextRetryAtMS, truncateError(lastErr), nowMs,
sourceUserOutbox, delivery.AppCode, delivery.EventID, delivery.Channel,
)
return err
}
// MarkCPFailed dead-letters a channel-level delivery after retry budget is exhausted.
func (r *MySQLRepository) MarkCPFailed(ctx context.Context, delivery CPDelivery, retryCount int, lastErr string, nowMs int64) error {
if r == nil || r.db == nil {
return xerr.New(xerr.Unavailable, "notice repository is not configured")
}
_, err := r.db.ExecContext(ctx, `
UPDATE notice_delivery_events
SET status = ?, retry_count = ?, locked_by = '', lock_until_ms = 0,
next_retry_at_ms = 0, last_error = ?, updated_at_ms = ?
WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?`,
deliveryStatusFailed, retryCount, truncateError(lastErr), nowMs,
sourceUserOutbox, delivery.AppCode, delivery.EventID, delivery.Channel,
)
return err
}
func validateDelivery(delivery CPDelivery) error {
if appcode.Normalize(delivery.AppCode) == "" || strings.TrimSpace(delivery.EventID) == "" ||
strings.TrimSpace(delivery.Channel) == "" || strings.TrimSpace(delivery.NoticeType) == "" ||
len(delivery.PayloadJSON) == 0 {
return xerr.New(xerr.InvalidArgument, "cp delivery is incomplete")
}
if delivery.Channel == channelTencentIMC2C && delivery.TargetUserID <= 0 {
return xerr.New(xerr.InvalidArgument, "cp c2c delivery target_user_id is required")
}
if delivery.Channel == channelTencentIMC2C && delivery.SenderUserID <= 0 {
return xerr.New(xerr.InvalidArgument, "cp c2c delivery sender_user_id is required")
}
if (delivery.Channel == channelTencentIMRoom || delivery.Channel == channelTencentIMRegion) && strings.TrimSpace(delivery.GroupID) == "" {
return xerr.New(xerr.InvalidArgument, "cp group delivery group_id is required")
}
return nil
}
func truncateError(value string) string {
value = strings.TrimSpace(value)
if len(value) <= 512 {
return value
}
return value[:512]
}

View File

@ -0,0 +1,153 @@
package cpnotice
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
// cpApplication mirrors the user-service CP outbox payload without importing user-service internals.
type cpApplication struct {
ApplicationID string
RelationType string
Status string
Requester cpUser
Target cpUser
RoomID string
RoomRegionID int64
Gift cpGift
ExpiresAtMS int64
DecidedAtMS int64
}
type cpUser struct {
UserID int64
DisplayUserID string
Username string
Avatar string
}
type cpGift struct {
GiftID string
GiftName string
GiftIconURL string
GiftAnimationURL string
GiftCount int64
GiftValue int64
BillingReceiptID string
}
func decodeObject(value string) (map[string]any, error) {
var payload map[string]any
if err := json.Unmarshal([]byte(strings.TrimSpace(value)), &payload); err != nil {
return nil, fmt.Errorf("invalid cp payload json: %w", err)
}
if payload == nil {
payload = map[string]any{}
}
return payload, nil
}
func applicationFromObject(obj map[string]any) (cpApplication, error) {
if len(obj) == 0 {
return cpApplication{}, fmt.Errorf("cp application payload is missing")
}
app := cpApplication{
ApplicationID: stringValue(obj, "application_id", "ApplicationID"),
RelationType: stringValue(obj, "relation_type", "RelationType"),
Status: stringValue(obj, "status", "Status"),
Requester: userFromObject(firstObject(obj, "requester", "Requester")),
Target: userFromObject(firstObject(obj, "target", "Target")),
RoomID: stringValue(obj, "room_id", "RoomID"),
RoomRegionID: int64Value(obj, "room_region_id", "RoomRegionID"),
Gift: giftFromObject(firstObject(obj, "gift", "Gift")),
ExpiresAtMS: int64Value(obj, "expires_at_ms", "ExpiresAtMS"),
DecidedAtMS: int64Value(obj, "decided_at_ms", "DecidedAtMS"),
}
if app.ApplicationID == "" || app.RelationType == "" || app.Requester.UserID <= 0 || app.Target.UserID <= 0 {
return cpApplication{}, fmt.Errorf("cp application payload is incomplete")
}
return app, nil
}
func userFromObject(obj map[string]any) cpUser {
return cpUser{
UserID: int64Value(obj, "user_id", "UserID"),
DisplayUserID: stringValue(obj, "display_user_id", "DisplayUserID"),
Username: stringValue(obj, "username", "Username"),
Avatar: stringValue(obj, "avatar", "Avatar"),
}
}
func giftFromObject(obj map[string]any) cpGift {
return cpGift{
GiftID: stringValue(obj, "gift_id", "GiftID"),
GiftName: stringValue(obj, "gift_name", "GiftName"),
GiftIconURL: stringValue(obj, "gift_icon_url", "GiftIconURL"),
GiftAnimationURL: stringValue(obj, "gift_animation_url", "GiftAnimationURL"),
GiftCount: int64Value(obj, "gift_count", "GiftCount"),
GiftValue: int64Value(obj, "gift_value", "GiftValue"),
BillingReceiptID: stringValue(obj, "billing_receipt_id", "BillingReceiptID"),
}
}
func firstObject(obj map[string]any, keys ...string) map[string]any {
for _, key := range keys {
if value, ok := obj[key]; ok {
if typed, ok := value.(map[string]any); ok {
return typed
}
// json.Unmarshal into map can leave nested structs only as maps; this defensive re-marshal keeps tests and
// future typed adapters from breaking the parser if they pass a struct-like object.
body, err := json.Marshal(value)
if err == nil {
var nested map[string]any
if json.Unmarshal(body, &nested) == nil {
return nested
}
}
}
}
return map[string]any{}
}
func stringValue(obj map[string]any, keys ...string) string {
for _, key := range keys {
switch value := obj[key].(type) {
case string:
return strings.TrimSpace(value)
case json.Number:
return value.String()
case float64:
if value == float64(int64(value)) {
return strconv.FormatInt(int64(value), 10)
}
case int64:
return strconv.FormatInt(value, 10)
case int:
return strconv.Itoa(value)
}
}
return ""
}
func int64Value(obj map[string]any, keys ...string) int64 {
for _, key := range keys {
switch value := obj[key].(type) {
case float64:
return int64(value)
case int64:
return value
case int:
return int64(value)
case json.Number:
parsed, _ := value.Int64()
return parsed
case string:
parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
return parsed
}
}
return 0
}

View File

@ -0,0 +1,407 @@
// Package cpnotice consumes user-service CP outbox facts and delivers the IM side effects.
package cpnotice
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strconv"
"time"
"hyapp/pkg/appcode"
"hyapp/pkg/imgroup"
"hyapp/pkg/logx"
"hyapp/pkg/tencentim"
"hyapp/pkg/usermq"
)
const (
eventApplicationCreated = "UserCPApplicationCreated"
eventApplicationAccepted = "UserCPApplicationAccepted"
eventApplicationRejected = "UserCPApplicationRejected"
eventRelationshipCreated = "UserCPRelationshipCreated"
channelTencentIMC2C = "tencent_im_c2c"
channelTencentIMRoom = "tencent_im_room"
channelTencentIMRegion = "tencent_im_region"
noticeTypeApplicationCreated = "cp_application_created"
noticeTypeApplicationAccepted = "cp_application_accepted"
noticeTypeApplicationRejected = "cp_application_rejected"
noticeTypeRelationshipCreated = "cp_relationship_created"
)
// Repository owns notice_delivery_events for CP IM deliveries; user-service remains the CP fact owner.
type Repository interface {
ClaimCPDelivery(ctx context.Context, workerID string, delivery CPDelivery, lockTTL time.Duration) (CPDelivery, bool, error)
MarkCPDelivered(ctx context.Context, delivery CPDelivery, deliveredPayload []byte, nowMs int64) error
MarkCPRetryable(ctx context.Context, delivery CPDelivery, retryCount int, nextRetryAtMS int64, lastErr string, nowMs int64) error
MarkCPFailed(ctx context.Context, delivery CPDelivery, retryCount int, lastErr string, nowMs int64) error
}
// Publisher is the Tencent IM boundary needed by CP notices.
type Publisher interface {
PublishUserCustomMessage(ctx context.Context, message tencentim.CustomUserMessage) error
PublishGroupCustomMessage(ctx context.Context, message tencentim.CustomGroupMessage) error
EnsureGroup(ctx context.Context, spec tencentim.GroupSpec) error
}
// Config carries process-level delivery settings that must match gateway broadcast group generation.
type Config struct {
NodeID string
GroupIDPrefix string
}
// Service maps CP facts to concrete IM messages and stores per-channel delivery state.
type Service struct {
cfg Config
repository Repository
publisher Publisher
}
// New creates CP notice service.
func New(cfg Config, repository Repository, publisher Publisher) *Service {
return &Service{cfg: cfg, repository: repository, publisher: publisher}
}
// ProcessUserOutboxMessage handles one user_outbox MQ message. Unknown events are acknowledged so that unrelated
// user-service facts can share the same topic without notice-service poisoning the consumer group.
func (s *Service) ProcessUserOutboxMessage(ctx context.Context, message usermq.UserOutboxMessage, options CPNoticeWorkerOptions) (bool, error) {
if !isCPNoticeEvent(message.EventType) {
return false, nil
}
options = normalizeCPNoticeWorkerOptions(options, s.cfg.NodeID)
if s.repository == nil {
return true, fmt.Errorf("notice repository is not configured")
}
if s.publisher == nil {
return true, fmt.Errorf("notice publisher is not configured")
}
deliveries, err := deliveriesFromMessage(message, s.cfg.GroupIDPrefix)
if err != nil {
return true, err
}
for _, delivery := range deliveries {
claimedDelivery, claimed, err := s.repository.ClaimCPDelivery(ctx, options.WorkerID, delivery, options.LockTTL)
if err != nil || !claimed {
return true, err
}
if err := s.publishDelivery(ctx, claimedDelivery, options); err != nil {
return true, err
}
}
return true, nil
}
func (s *Service) publishDelivery(ctx context.Context, delivery CPDelivery, options CPNoticeWorkerOptions) error {
publishCtx, cancel := context.WithTimeout(ctx, options.PublishTimeout)
defer cancel()
var err error
switch delivery.Channel {
case channelTencentIMC2C:
err = s.publisher.PublishUserCustomMessage(publishCtx, tencentim.CustomUserMessage{
ToAccount: tencentim.FormatUserID(delivery.TargetUserID),
FromAccount: tencentim.FormatUserID(delivery.SenderUserID),
EventID: delivery.EventID,
Desc: delivery.NoticeType,
Ext: "cp_relation_notice",
PayloadJSON: delivery.PayloadJSON,
})
case channelTencentIMRoom:
err = s.publisher.PublishGroupCustomMessage(publishCtx, tencentim.CustomGroupMessage{
GroupID: delivery.GroupID,
EventID: delivery.EventID,
Desc: delivery.NoticeType,
Ext: "cp_room_notice",
PayloadJSON: delivery.PayloadJSON,
})
case channelTencentIMRegion:
// 区域群是长期播报群;发送前幂等创建,避免本地真实测试或新区域缺群导致关系主流程事实无法通知。
if ensureErr := s.publisher.EnsureGroup(publishCtx, tencentim.GroupSpec{
GroupID: delivery.GroupID,
Name: delivery.GroupID,
Type: tencentim.DefaultGroupType,
ApplyJoinOption: "FreeAccess",
}); ensureErr != nil {
err = ensureErr
break
}
err = s.publisher.PublishGroupCustomMessage(publishCtx, tencentim.CustomGroupMessage{
GroupID: delivery.GroupID,
EventID: delivery.EventID,
Desc: delivery.NoticeType,
Ext: "cp_region_broadcast",
PayloadJSON: delivery.PayloadJSON,
})
default:
err = fmt.Errorf("unsupported cp notice channel %q", delivery.Channel)
}
nowMs := time.Now().UnixMilli()
if err == nil {
if markErr := s.repository.MarkCPDelivered(ctx, delivery, delivery.PayloadJSON, nowMs); markErr != nil {
return markErr
}
logx.Info(ctx, "notice_cp_delivered",
slog.String("event_id", delivery.EventID),
slog.String("app_code", delivery.AppCode),
slog.String("notice_type", delivery.NoticeType),
slog.String("channel", delivery.Channel),
slog.Int64("target_user_id", delivery.TargetUserID),
slog.String("group_id", delivery.GroupID),
)
return nil
}
return s.markFailed(ctx, delivery, options, err)
}
func (s *Service) markFailed(ctx context.Context, delivery CPDelivery, options CPNoticeWorkerOptions, cause error) error {
nowMs := time.Now().UnixMilli()
nextRetryCount := delivery.RetryCount + 1
if nextRetryCount >= options.MaxRetryCount {
if err := s.repository.MarkCPFailed(ctx, delivery, nextRetryCount, cause.Error(), nowMs); err != nil {
return err
}
logx.Error(ctx, "notice_cp_dead_letter", cause,
slog.String("event_id", delivery.EventID),
slog.String("app_code", delivery.AppCode),
slog.String("notice_type", delivery.NoticeType),
slog.String("channel", delivery.Channel),
slog.Int("retry_count", nextRetryCount),
)
return nil
}
nextRetryAtMS := nowMs + cpNoticeBackoff(nextRetryCount, options).Milliseconds()
if err := s.repository.MarkCPRetryable(ctx, delivery, nextRetryCount, nextRetryAtMS, cause.Error(), nowMs); err != nil {
return err
}
logx.Error(ctx, "notice_cp_retryable", cause,
slog.String("event_id", delivery.EventID),
slog.String("app_code", delivery.AppCode),
slog.String("notice_type", delivery.NoticeType),
slog.String("channel", delivery.Channel),
slog.Int("retry_count", nextRetryCount),
slog.Int64("next_retry_at_ms", nextRetryAtMS),
)
return nil
}
func isCPNoticeEvent(eventType string) bool {
switch eventType {
case eventApplicationCreated, eventApplicationAccepted, eventApplicationRejected, eventRelationshipCreated:
return true
default:
return false
}
}
func deliveriesFromMessage(message usermq.UserOutboxMessage, groupIDPrefix string) ([]CPDelivery, error) {
appCode := appcode.Normalize(message.AppCode)
root, err := decodeObject(message.PayloadJSON)
if err != nil {
return nil, err
}
switch message.EventType {
case eventApplicationCreated:
application, err := applicationFromObject(root)
if err != nil {
return nil, err
}
basePayload, err := cpApplicationPayload(message, application, noticeTypeApplicationCreated, "pending")
if err != nil {
return nil, err
}
roomPayload := cloneJSONMap(basePayload)
roomPayload["recipient_user_id"] = strconv.FormatInt(application.Target.UserID, 10)
return []CPDelivery{
{
AppCode: appCode,
EventID: message.EventID,
EventType: message.EventType,
Channel: channelTencentIMC2C,
NoticeType: noticeTypeApplicationCreated,
SenderUserID: application.Requester.UserID,
TargetUserID: application.Target.UserID,
PayloadJSON: mustJSON(basePayload),
CreatedAtMS: message.OccurredAtMS,
},
{
AppCode: appCode,
EventID: message.EventID,
EventType: message.EventType,
Channel: channelTencentIMRoom,
NoticeType: noticeTypeApplicationCreated,
TargetUserID: application.Target.UserID,
GroupID: application.RoomID,
PayloadJSON: mustJSON(roomPayload),
CreatedAtMS: message.OccurredAtMS,
},
}, nil
case eventApplicationAccepted:
application, err := applicationFromObject(root)
if err != nil {
return nil, err
}
payload, err := cpApplicationPayload(message, application, noticeTypeApplicationAccepted, "accepted")
if err != nil {
return nil, err
}
return applicationDecisionDeliveries(appCode, message, application, payload, noticeTypeApplicationAccepted), nil
case eventApplicationRejected:
application, err := applicationFromObject(firstObject(root, "application", "Application"))
if err != nil {
return nil, err
}
payload, err := cpApplicationPayload(message, application, noticeTypeApplicationRejected, "rejected")
if err != nil {
return nil, err
}
payload["reason"] = stringValue(root, "reason", "Reason")
return applicationDecisionDeliveries(appCode, message, application, payload, noticeTypeApplicationRejected), nil
case eventRelationshipCreated:
application, err := applicationFromObject(firstObject(root, "application", "Application"))
if err != nil {
return nil, err
}
relationship := firstObject(root, "relationship", "Relationship")
relationshipID := stringValue(relationship, "relationship_id", "RelationshipID")
groupID, err := imgroup.RegionBroadcastGroupIDWithPrefix(groupIDPrefix, appCode, application.RoomRegionID)
if err != nil {
return nil, err
}
return relationshipCreatedDelivery(appCode, message, application, relationshipID, groupID)
default:
return nil, nil
}
}
func applicationDecisionDeliveries(appCode string, message usermq.UserOutboxMessage, application cpApplication, payload map[string]any, noticeType string) []CPDelivery {
// 同意/拒绝结果必须同步覆盖两个入口:
// 1. A 在私聊里看到 C2C 结果;
// 2. A 在申请来源房间上下文看到房间 IM客户端用 recipient_user_id 只给 A 展示按钮态变化。
deliveries := []CPDelivery{{
AppCode: appCode,
EventID: message.EventID,
EventType: message.EventType,
Channel: channelTencentIMC2C,
NoticeType: noticeType,
SenderUserID: application.Target.UserID,
TargetUserID: application.Requester.UserID,
PayloadJSON: mustJSON(payload),
CreatedAtMS: message.OccurredAtMS,
}}
if application.RoomID == "" {
return deliveries
}
roomPayload := cloneJSONMap(payload)
roomPayload["recipient_user_id"] = strconv.FormatInt(application.Requester.UserID, 10)
deliveries = append(deliveries, CPDelivery{
AppCode: appCode,
EventID: message.EventID,
EventType: message.EventType,
Channel: channelTencentIMRoom,
NoticeType: noticeType,
TargetUserID: application.Requester.UserID,
GroupID: application.RoomID,
PayloadJSON: mustJSON(roomPayload),
CreatedAtMS: message.OccurredAtMS,
})
return deliveries
}
func relationshipCreatedDelivery(appCode string, message usermq.UserOutboxMessage, application cpApplication, relationshipID string, groupID string) ([]CPDelivery, error) {
payload, err := cpRelationshipBroadcastPayload(message, application, relationshipID, groupID)
if err != nil {
return nil, err
}
return []CPDelivery{{
AppCode: appCode,
EventID: message.EventID,
EventType: message.EventType,
Channel: channelTencentIMRegion,
NoticeType: noticeTypeRelationshipCreated,
TargetUserID: 0,
GroupID: groupID,
PayloadJSON: mustJSON(payload),
CreatedAtMS: message.OccurredAtMS,
}}, nil
}
func cpApplicationPayload(message usermq.UserOutboxMessage, application cpApplication, noticeType string, status string) (map[string]any, error) {
if application.ApplicationID == "" || application.Requester.UserID <= 0 || application.Target.UserID <= 0 {
return nil, fmt.Errorf("cp application payload is incomplete")
}
return map[string]any{
"event_id": message.EventID,
"event_type": noticeType,
"source_event_type": message.EventType,
"app_code": appcode.Normalize(message.AppCode),
"application_id": application.ApplicationID,
"relation_type": application.RelationType,
"status": status,
"requester": userPayload(application.Requester),
"target": userPayload(application.Target),
"room_id": application.RoomID,
"room_region_id": application.RoomRegionID,
"gift": giftPayload(application.Gift),
"expires_at_ms": application.ExpiresAtMS,
"decided_at_ms": application.DecidedAtMS,
"source_created_at_ms": message.OccurredAtMS,
}, nil
}
func cpRelationshipBroadcastPayload(message usermq.UserOutboxMessage, application cpApplication, relationshipID string, groupID string) (map[string]any, error) {
if application.Requester.UserID <= 0 || application.Target.UserID <= 0 || application.RoomRegionID <= 0 {
return nil, fmt.Errorf("cp relationship broadcast payload is incomplete")
}
return map[string]any{
"event_id": message.EventID,
"event_type": noticeTypeRelationshipCreated,
"source_event_type": message.EventType,
"app_code": appcode.Normalize(message.AppCode),
"relationship_id": relationshipID,
"application_id": application.ApplicationID,
"relation_type": application.RelationType,
"requester": userPayload(application.Requester),
"target": userPayload(application.Target),
"gift": giftPayload(application.Gift),
"room_id": application.RoomID,
"room_region_id": application.RoomRegionID,
"group_id": groupID,
"source_created_at_ms": message.OccurredAtMS,
}, nil
}
func userPayload(user cpUser) map[string]any {
return map[string]any{
"user_id": strconv.FormatInt(user.UserID, 10),
"display_user_id": user.DisplayUserID,
"username": user.Username,
"avatar": user.Avatar,
}
}
func giftPayload(gift cpGift) map[string]any {
return map[string]any{
"gift_id": gift.GiftID,
"gift_name": gift.GiftName,
"gift_icon_url": gift.GiftIconURL,
"gift_animation_url": gift.GiftAnimationURL,
"gift_count": gift.GiftCount,
"gift_value": gift.GiftValue,
"billing_receipt_id": gift.BillingReceiptID,
}
}
func mustJSON(payload map[string]any) json.RawMessage {
body, _ := json.Marshal(payload)
return body
}
func cloneJSONMap(input map[string]any) map[string]any {
out := make(map[string]any, len(input))
for key, value := range input {
out[key] = value
}
return out
}

View File

@ -0,0 +1,229 @@
package cpnotice
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
"hyapp/pkg/tencentim"
"hyapp/pkg/usermq"
)
func TestProcessApplicationCreatedPublishesC2CAndRoomIM(t *testing.T) {
repo := &fakeRepository{}
publisher := &fakePublisher{}
service := New(Config{NodeID: "notice-test"}, repo, publisher)
handled, err := service.ProcessUserOutboxMessage(context.Background(), usermq.UserOutboxMessage{
AppCode: "lalu",
EventID: "cp_application:created:1002:1",
EventType: eventApplicationCreated,
AggregateType: "cp_application",
AggregateID: 1002,
PayloadJSON: applicationPayloadJSON(),
OccurredAtMS: 1700000000000,
}, CPNoticeWorkerOptions{PublishTimeout: time.Second})
if err != nil || !handled {
t.Fatalf("ProcessUserOutboxMessage failed handled=%v err=%v", handled, err)
}
if len(publisher.userMessages) != 1 || publisher.userMessages[0].FromAccount != "1001" || publisher.userMessages[0].ToAccount != "1002" {
t.Fatalf("expected target c2c message, got %+v", publisher.userMessages)
}
if len(publisher.groupMessages) != 1 || publisher.groupMessages[0].GroupID != "room-100" {
t.Fatalf("expected room group message, got %+v", publisher.groupMessages)
}
if len(repo.delivered) != 2 || repo.delivered[0].Channel != channelTencentIMC2C || repo.delivered[1].Channel != channelTencentIMRoom {
t.Fatalf("delivery channels mismatch: %+v", repo.delivered)
}
}
func TestProcessApplicationAcceptedPublishesRequesterC2CAndRoomIM(t *testing.T) {
repo := &fakeRepository{}
publisher := &fakePublisher{}
service := New(Config{NodeID: "notice-test"}, repo, publisher)
handled, err := service.ProcessUserOutboxMessage(context.Background(), usermq.UserOutboxMessage{
AppCode: "lalu",
EventID: "cp_application:accepted:1002:1",
EventType: eventApplicationAccepted,
AggregateType: "cp_application",
AggregateID: 1002,
PayloadJSON: applicationPayloadJSON(),
OccurredAtMS: 1700000000001,
}, CPNoticeWorkerOptions{PublishTimeout: time.Second})
if err != nil || !handled {
t.Fatalf("ProcessUserOutboxMessage failed handled=%v err=%v", handled, err)
}
if len(publisher.userMessages) != 1 || publisher.userMessages[0].FromAccount != "1002" || publisher.userMessages[0].ToAccount != "1001" {
t.Fatalf("expected requester c2c message, got %+v", publisher.userMessages)
}
if len(publisher.groupMessages) != 1 || publisher.groupMessages[0].GroupID != "room-100" {
t.Fatalf("expected requester room group message, got %+v", publisher.groupMessages)
}
if len(repo.delivered) != 2 || repo.delivered[0].Channel != channelTencentIMC2C || repo.delivered[1].Channel != channelTencentIMRoom {
t.Fatalf("delivery channels mismatch: %+v", repo.delivered)
}
}
func TestProcessApplicationRejectedPublishesRequesterC2CAndRoomIM(t *testing.T) {
repo := &fakeRepository{}
publisher := &fakePublisher{}
service := New(Config{NodeID: "notice-test"}, repo, publisher)
body, _ := json.Marshal(map[string]any{
"application": mustObject(applicationPayloadJSON()),
"reason": "not_now",
})
handled, err := service.ProcessUserOutboxMessage(context.Background(), usermq.UserOutboxMessage{
AppCode: "lalu",
EventID: "cp_application:rejected:1002:1",
EventType: eventApplicationRejected,
AggregateType: "cp_application",
AggregateID: 1002,
PayloadJSON: string(body),
OccurredAtMS: 1700000000001,
}, CPNoticeWorkerOptions{PublishTimeout: time.Second})
if err != nil || !handled {
t.Fatalf("ProcessUserOutboxMessage failed handled=%v err=%v", handled, err)
}
if len(publisher.userMessages) != 1 || publisher.userMessages[0].FromAccount != "1002" || publisher.userMessages[0].ToAccount != "1001" {
t.Fatalf("expected requester c2c message, got %+v", publisher.userMessages)
}
if len(publisher.groupMessages) != 1 || publisher.groupMessages[0].GroupID != "room-100" {
t.Fatalf("expected requester room group message, got %+v", publisher.groupMessages)
}
if len(repo.delivered) != 2 || repo.delivered[0].NoticeType != noticeTypeApplicationRejected || repo.delivered[1].Channel != channelTencentIMRoom {
t.Fatalf("delivery channels mismatch: %+v", repo.delivered)
}
}
func TestProcessRelationshipCreatedPublishesRegionIM(t *testing.T) {
repo := &fakeRepository{}
publisher := &fakePublisher{}
service := New(Config{NodeID: "notice-test", GroupIDPrefix: "test_"}, repo, publisher)
payload := map[string]any{
"application": mustObject(applicationPayloadJSON()),
"relationship": map[string]any{
"RelationshipID": "cp_rel:1001:1002",
},
}
body, _ := json.Marshal(payload)
handled, err := service.ProcessUserOutboxMessage(context.Background(), usermq.UserOutboxMessage{
AppCode: "lalu",
EventID: "cp_relationship:created:1002:1",
EventType: eventRelationshipCreated,
AggregateType: "cp_relationship",
AggregateID: 1002,
PayloadJSON: string(body),
OccurredAtMS: 1700000000002,
}, CPNoticeWorkerOptions{PublishTimeout: time.Second})
if err != nil || !handled {
t.Fatalf("ProcessUserOutboxMessage failed handled=%v err=%v", handled, err)
}
if len(publisher.ensuredGroups) != 1 || publisher.ensuredGroups[0].GroupID != "test_hy_lalu_bc_r_86" {
t.Fatalf("expected region group ensure, got %+v", publisher.ensuredGroups)
}
if len(publisher.groupMessages) != 1 || publisher.groupMessages[0].GroupID != "test_hy_lalu_bc_r_86" {
t.Fatalf("expected region group message, got %+v", publisher.groupMessages)
}
if len(publisher.userMessages) != 0 {
t.Fatalf("relationship created should not publish c2c: %+v", publisher.userMessages)
}
}
func TestPublishFailureMarksRetryable(t *testing.T) {
repo := &fakeRepository{}
publisher := &fakePublisher{err: errors.New("im timeout")}
service := New(Config{NodeID: "notice-test"}, repo, publisher)
_, err := service.ProcessUserOutboxMessage(context.Background(), usermq.UserOutboxMessage{
AppCode: "lalu",
EventID: "cp_application:created:1002:retry",
EventType: eventApplicationCreated,
AggregateType: "cp_application",
AggregateID: 1002,
PayloadJSON: applicationPayloadJSON(),
OccurredAtMS: 1700000000003,
}, CPNoticeWorkerOptions{PublishTimeout: time.Second, MaxRetryCount: 3})
if err != nil {
t.Fatalf("retryable publish should be recorded without returning poison error: %v", err)
}
if len(repo.retryable) == 0 || repo.retryable[0].RetryCount != 1 {
t.Fatalf("expected retryable delivery, got %+v", repo.retryable)
}
}
type fakeRepository struct {
delivered []CPDelivery
retryable []CPDelivery
failed []CPDelivery
}
func (r *fakeRepository) ClaimCPDelivery(_ context.Context, _ string, delivery CPDelivery, _ time.Duration) (CPDelivery, bool, error) {
return delivery, true, nil
}
func (r *fakeRepository) MarkCPDelivered(_ context.Context, delivery CPDelivery, _ []byte, _ int64) error {
r.delivered = append(r.delivered, delivery)
return nil
}
func (r *fakeRepository) MarkCPRetryable(_ context.Context, delivery CPDelivery, retryCount int, _ int64, _ string, _ int64) error {
delivery.RetryCount = retryCount
r.retryable = append(r.retryable, delivery)
return nil
}
func (r *fakeRepository) MarkCPFailed(_ context.Context, delivery CPDelivery, retryCount int, _ string, _ int64) error {
delivery.RetryCount = retryCount
r.failed = append(r.failed, delivery)
return nil
}
type fakePublisher struct {
userMessages []tencentim.CustomUserMessage
groupMessages []tencentim.CustomGroupMessage
ensuredGroups []tencentim.GroupSpec
err error
}
func (p *fakePublisher) PublishUserCustomMessage(_ context.Context, message tencentim.CustomUserMessage) error {
p.userMessages = append(p.userMessages, message)
return p.err
}
func (p *fakePublisher) PublishGroupCustomMessage(_ context.Context, message tencentim.CustomGroupMessage) error {
p.groupMessages = append(p.groupMessages, message)
return p.err
}
func (p *fakePublisher) EnsureGroup(_ context.Context, spec tencentim.GroupSpec) error {
p.ensuredGroups = append(p.ensuredGroups, spec)
return p.err
}
func applicationPayloadJSON() string {
return `{
"ApplicationID":"cp_app:event-1",
"RelationType":"cp",
"Status":"pending",
"Requester":{"UserID":1001,"DisplayUserID":"163001","Username":"Alice","Avatar":"https://cdn.example/a.png"},
"Target":{"UserID":1002,"DisplayUserID":"163002","Username":"Bob","Avatar":"https://cdn.example/b.png"},
"RoomID":"room-100",
"RoomRegionID":86,
"Gift":{"GiftID":"cp_ring","GiftName":"CP Ring","GiftIconURL":"https://cdn.example/gift.png","GiftAnimationURL":"","GiftCount":1,"GiftValue":100,"BillingReceiptID":"tx-1"},
"ExpiresAtMS":1700003600000,
"DecidedAtMS":0
}`
}
func mustObject(value string) map[string]any {
var out map[string]any
if err := json.Unmarshal([]byte(value), &out); err != nil {
panic(err)
}
return out
}

View File

@ -0,0 +1,59 @@
package cpnotice
import (
"strings"
"time"
)
// CPNoticeWorkerOptions mirrors other notice workers but is MQ-driven; the lock still protects per-channel delivery rows.
type CPNoticeWorkerOptions struct {
WorkerID string
BatchSize int
LockTTL time.Duration
PublishTimeout time.Duration
MaxRetryCount int
InitialBackoff time.Duration
MaxBackoff time.Duration
}
func normalizeCPNoticeWorkerOptions(options CPNoticeWorkerOptions, nodeID string) CPNoticeWorkerOptions {
if strings.TrimSpace(options.WorkerID) == "" {
options.WorkerID = strings.TrimSpace(nodeID)
}
if options.WorkerID == "" {
options.WorkerID = "notice-cp-worker"
}
if options.BatchSize <= 0 {
options.BatchSize = 100
}
if options.LockTTL <= 0 {
options.LockTTL = 30 * time.Second
}
if options.PublishTimeout <= 0 {
options.PublishTimeout = 3 * time.Second
}
if options.MaxRetryCount <= 0 {
options.MaxRetryCount = 10
}
if options.InitialBackoff <= 0 {
options.InitialBackoff = 5 * time.Second
}
if options.MaxBackoff <= 0 {
options.MaxBackoff = 5 * time.Minute
}
return options
}
func cpNoticeBackoff(retryCount int, options CPNoticeWorkerOptions) time.Duration {
if retryCount <= 0 {
return options.InitialBackoff
}
backoff := options.InitialBackoff
for i := 1; i < retryCount; i++ {
backoff *= 2
if backoff >= options.MaxBackoff {
return options.MaxBackoff
}
}
return backoff
}

View File

@ -179,6 +179,11 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
CountryId: cmd.SenderCountryID,
RegionId: firstNonZeroInt64(cmd.SenderRegionID, roomMeta.VisibleRegionID),
CommandId: targetBilling.CommandID,
GiftTypeCode: targetBilling.Billing.GetGiftTypeCode(),
CpRelationType: targetBilling.Billing.GetCpRelationType(),
GiftName: targetBilling.Billing.GetGiftName(),
GiftIconUrl: targetBilling.Billing.GetGiftIconUrl(),
GiftAnimationUrl: targetBilling.Billing.GetGiftAnimationUrl(),
})
if err != nil {
return mutationResult{}, nil, err
@ -267,6 +272,11 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
"price_version": settledCommand.PriceVersion,
"host_period_diamond_added": fmt.Sprintf("%d", settledCommand.HostPeriodDiamondAdded),
"host_period_cycle_key": settledCommand.HostPeriodCycleKey,
"gift_type_code": billing.GetGiftTypeCode(),
"cp_relation_type": billing.GetCpRelationType(),
"gift_name": billing.GetGiftName(),
"gift_icon_url": billing.GetGiftIconUrl(),
"gift_animation_url": billing.GetGiftAnimationUrl(),
},
},
}, records, nil

View File

@ -27,6 +27,9 @@ invite_recharge_worker:
enabled: false
mic_time_worker:
enabled: false
cp_relationship_worker:
enabled: true
consumer_group: "hyapp-user-cp-room-outbox"
outbox_worker:
enabled: true
poll_interval: "1s"
@ -46,7 +49,7 @@ rocketmq:
consumer_group: "hyapp-user-invite-wallet-outbox"
consumer_max_reconsume_times: 16
room_outbox:
enabled: false
enabled: true
topic: "hyapp_room_outbox"
consumer_group: "hyapp-user-mictime-room-outbox"
consumer_max_reconsume_times: 16

View File

@ -27,6 +27,9 @@ invite_recharge_worker:
enabled: true
mic_time_worker:
enabled: true
cp_relationship_worker:
enabled: true
consumer_group: "hyapp-user-cp-room-outbox"
outbox_worker:
enabled: true
poll_interval: "1s"

View File

@ -27,6 +27,9 @@ invite_recharge_worker:
enabled: false
mic_time_worker:
enabled: false
cp_relationship_worker:
enabled: false
consumer_group: "hyapp-user-cp-room-outbox"
outbox_worker:
enabled: false
poll_interval: "1s"

View File

@ -395,6 +395,95 @@ CREATE TABLE IF NOT EXISTS user_invite_counters (
PRIMARY KEY (app_code, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户邀请计数表';
CREATE TABLE IF NOT EXISTS user_cp_relation_configs (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
relation_type VARCHAR(32) NOT NULL COMMENT '关系类型cp、brother、sister',
max_count_per_user INT NOT NULL COMMENT '每个用户可拥有的该类型 active 关系数量上限',
application_expire_hours INT NOT NULL COMMENT '申请过期小时数',
status VARCHAR(32) NOT NULL DEFAULT 'active' 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, relation_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 CP 关系配置表';
CREATE TABLE IF NOT EXISTS user_cp_level_rules (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
relation_type VARCHAR(32) NOT NULL COMMENT '关系类型cp、brother、sister',
level_no INT NOT NULL COMMENT '关系等级1-5',
intimacy_threshold BIGINT NOT NULL COMMENT '达到该等级需要的亲密值',
reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '升级发放的装扮资源组 ID0 表示不发放',
level_icon_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '等级展示图标',
status VARCHAR(32) NOT NULL DEFAULT 'active' 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, relation_type, level_no),
KEY idx_user_cp_level_rules_status (app_code, status, relation_type, level_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 CP 等级规则表';
CREATE TABLE IF NOT EXISTS user_cp_applications (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
application_id VARCHAR(128) NOT NULL COMMENT '申请 ID',
requester_user_id BIGINT NOT NULL COMMENT '发起用户 ID',
target_user_id BIGINT NOT NULL COMMENT '接收用户 ID',
relation_type VARCHAR(32) NOT NULL COMMENT '关系类型cp、brother、sister',
status VARCHAR(32) NOT NULL COMMENT '申请状态pending、accepted、rejected、expired、blocked',
room_id VARCHAR(64) NOT NULL DEFAULT '' COMMENT '触发礼物所在房间 ID',
room_region_id BIGINT NOT NULL DEFAULT 0 COMMENT '触发礼物所在房间区域 ID',
gift_id VARCHAR(64) NOT NULL DEFAULT '' COMMENT '触发申请的礼物 ID',
gift_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '触发申请的礼物名称',
gift_icon_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '触发申请的礼物图标',
gift_animation_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '触发申请的礼物动画',
gift_count INT NOT NULL DEFAULT 0 COMMENT '触发申请的礼物数量',
gift_value BIGINT NOT NULL DEFAULT 0 COMMENT '触发申请的礼物亲密值/热度值',
billing_receipt_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '钱包扣费回执 ID',
source_room_event_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '来源 room outbox 事件 ID',
source_command_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '来源房间命令 ID',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
expires_at_ms BIGINT NOT NULL COMMENT '过期时间UTC epoch ms',
decided_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '同意或拒绝时间UTC epoch ms',
PRIMARY KEY (app_code, application_id),
UNIQUE KEY uk_user_cp_app_source_event (app_code, source_room_event_id),
KEY idx_user_cp_app_target_status (app_code, target_user_id, status, updated_at_ms),
KEY idx_user_cp_app_requester_status (app_code, requester_user_id, status, updated_at_ms),
KEY idx_user_cp_app_pair_status (app_code, requester_user_id, target_user_id, status, relation_type),
KEY idx_user_cp_app_expire (app_code, status, expires_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 CP 关系申请表';
CREATE TABLE IF NOT EXISTS user_cp_relationships (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
relationship_id VARCHAR(128) NOT NULL COMMENT '关系 ID',
user_a_id BIGINT NOT NULL COMMENT '较小的用户 ID',
user_b_id BIGINT NOT NULL COMMENT '较大的用户 ID',
relation_type VARCHAR(32) NOT NULL COMMENT '关系类型cp、brother、sister',
status VARCHAR(32) NOT NULL COMMENT '关系状态active、ended',
intimacy_value BIGINT NOT NULL DEFAULT 0 COMMENT '两人关系亲密值',
level_no INT NOT NULL DEFAULT 1 COMMENT '当前关系等级',
source_application_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '建立关系的申请 ID',
formed_at_ms BIGINT NOT NULL COMMENT '建立时间UTC epoch ms',
ended_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '解除时间UTC epoch ms',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, relationship_id),
UNIQUE KEY uk_user_cp_rel_active_pair (app_code, user_a_id, user_b_id, status),
KEY idx_user_cp_rel_user_a (app_code, user_a_id, status, relation_type, updated_at_ms),
KEY idx_user_cp_rel_user_b (app_code, user_b_id, status, relation_type, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 CP 关系表';
CREATE TABLE IF NOT EXISTS user_cp_gift_event_consumption (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
room_event_id VARCHAR(128) NOT NULL COMMENT 'room outbox 事件 ID',
status VARCHAR(32) NOT NULL COMMENT '消费状态',
application_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '生成或刷新申请 ID',
relationship_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '命中的 active 关系 ID',
skip_reason VARCHAR(128) NOT NULL DEFAULT '' COMMENT '跳过原因',
consumed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '消费时间UTC epoch ms',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, room_event_id),
KEY idx_user_cp_gift_consumption_status (app_code, status, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 CP 礼物事件消费位点表';
CREATE TABLE IF NOT EXISTS user_outbox (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
event_id VARCHAR(128) NOT NULL COMMENT '事件幂等 ID',
@ -1235,6 +1324,8 @@ CREATE TABLE IF NOT EXISTS auth_sessions (
refresh_token_hash VARCHAR(128) NOT NULL COMMENT '刷新令牌哈希值',
device_id VARCHAR(128) NOT NULL COMMENT '设备 ID',
expires_at_ms BIGINT NOT NULL COMMENT '过期时间UTC epoch ms',
last_heartbeat_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '最近 App 心跳时间UTC epoch ms',
last_heartbeat_request_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '最近 App 心跳请求 ID',
revoked_at_ms BIGINT NULL COMMENT '吊销时间UTC epoch ms',
revoked_reason VARCHAR(64) NOT NULL DEFAULT '' COMMENT '吊销原因',
revoked_request_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '吊销请求 ID',
@ -1243,9 +1334,38 @@ CREATE TABLE IF NOT EXISTS auth_sessions (
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
UNIQUE KEY uk_auth_sessions_refresh_token_hash (app_code, refresh_token_hash),
KEY idx_auth_sessions_user_id (app_code, user_id),
KEY idx_auth_sessions_heartbeat (app_code, last_heartbeat_at_ms),
KEY idx_auth_sessions_expires_at (app_code, expires_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='认证会话表';
-- 老环境已经存在 auth_sessions 时CREATE TABLE IF NOT EXISTS 不会补 App 心跳字段。
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_sessions' AND COLUMN_NAME = 'last_heartbeat_at_ms') = 0,
'ALTER TABLE auth_sessions ADD COLUMN last_heartbeat_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT ''最近 App 心跳时间UTC epoch ms'' AFTER expires_at_ms',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_sessions' AND COLUMN_NAME = 'last_heartbeat_request_id') = 0,
'ALTER TABLE auth_sessions ADD COLUMN last_heartbeat_request_id VARCHAR(96) NOT NULL DEFAULT '''' COMMENT ''最近 App 心跳请求 ID'' AFTER last_heartbeat_at_ms',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_sessions' AND INDEX_NAME = 'idx_auth_sessions_heartbeat') = 0,
'ALTER TABLE auth_sessions ADD INDEX idx_auth_sessions_heartbeat (app_code, last_heartbeat_at_ms)',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS login_audit (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键 ID',

View File

@ -27,11 +27,13 @@ import (
"hyapp/pkg/usermq"
"hyapp/pkg/walletmq"
"hyapp/services/user-service/internal/config"
cpdomain "hyapp/services/user-service/internal/domain/cp"
invitedomain "hyapp/services/user-service/internal/domain/invite"
mictimedomain "hyapp/services/user-service/internal/domain/mictime"
"hyapp/services/user-service/internal/integration/activityclient"
"hyapp/services/user-service/internal/integration/roomclient"
authservice "hyapp/services/user-service/internal/service/auth"
cpservice "hyapp/services/user-service/internal/service/cp"
hostservice "hyapp/services/user-service/internal/service/host"
inviteservice "hyapp/services/user-service/internal/service/invite"
mictimeservice "hyapp/services/user-service/internal/service/mictime"
@ -71,6 +73,8 @@ type App struct {
inviteSvc *inviteservice.Service
// micTimeSvc 消费 room mic outbox维护用户维度麦上时长 read model。
micTimeSvc *mictimeservice.Service
// cpSvc 消费 room gift outbox维护 CP/兄弟/姐妹申请和亲密值。
cpSvc *cpservice.Service
// loginRiskRedisClose 关闭登录风险 Redis cachecache 不作为启动硬依赖。
loginRiskRedisClose func() error
// cfg 保存 worker 运行参数,避免 Run 阶段重新读配置。
@ -123,6 +127,7 @@ func New(cfg config.Config) (*App, error) {
hostRepo := mysqlRepo.HostRepository()
inviteRepo := mysqlRepo.InviteRepository()
micTimeRepo := mysqlRepo.MicTimeRepository()
cpRepo := mysqlRepo.CPRepository()
var ipDecisionCache authservice.IPDecisionCache
var loginRiskRedisClose func() error
if cfg.LoginRisk.Enabled && cfg.LoginRisk.RedisAddr != "" {
@ -242,7 +247,8 @@ func New(cfg config.Config) (*App, error) {
)
inviteSvc := inviteservice.New(inviteRepo)
micTimeSvc := mictimeservice.New(micTimeRepo)
mqConsumers, err := newMQConsumers(cfg, inviteSvc, micTimeSvc)
cpSvc := cpservice.New(cpRepo)
mqConsumers, err := newMQConsumers(cfg, inviteSvc, micTimeSvc, cpSvc)
if err != nil {
if loginRiskRedisClose != nil {
_ = loginRiskRedisClose()
@ -278,6 +284,7 @@ func New(cfg config.Config) (*App, error) {
}
userServer := grpcserver.NewServer(authSvc, userSvc, hostSvc)
userServer.SetMicTimeService(micTimeSvc)
userServer.SetCPService(cpSvc)
// 多个 protobuf service 共用一个 Server 适配器,领域逻辑仍拆在 auth/user/host service。
userv1.RegisterAuthServiceServer(server, userServer)
userv1.RegisterUserServiceServer(server, userServer)
@ -291,6 +298,8 @@ func New(cfg config.Config) (*App, error) {
userv1.RegisterUserHostServiceServer(server, userServer)
userv1.RegisterUserHostAdminServiceServer(server, userServer)
userv1.RegisterUserCronServiceServer(server, userServer)
userv1.RegisterUserCPServiceServer(server, userServer)
userv1.RegisterUserCPInternalServiceServer(server, userServer)
healthDependencies := []grpchealth.Dependency{{
Name: "mysql",
@ -331,6 +340,7 @@ func New(cfg config.Config) (*App, error) {
userSvc: userSvc,
inviteSvc: inviteSvc,
micTimeSvc: micTimeSvc,
cpSvc: cpSvc,
loginRiskRedisClose: loginRiskRedisClose,
cfg: cfg,
workerCtx: workerCtx,
@ -524,8 +534,8 @@ func (i tencentIMAccountImporter) ImportAccount(ctx context.Context, userID int6
})
}
func newMQConsumers(cfg config.Config, inviteSvc *inviteservice.Service, micTimeSvc *mictimeservice.Service) ([]*rocketmqx.Consumer, error) {
consumers := make([]*rocketmqx.Consumer, 0, 2)
func newMQConsumers(cfg config.Config, inviteSvc *inviteservice.Service, micTimeSvc *mictimeservice.Service, cpSvc *cpservice.Service) ([]*rocketmqx.Consumer, error) {
consumers := make([]*rocketmqx.Consumer, 0, 3)
if cfg.InviteRechargeWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled {
consumer, err := rocketmqx.NewConsumer(walletOutboxConsumerConfig(cfg.RocketMQ))
if err != nil {
@ -564,6 +574,27 @@ func newMQConsumers(cfg config.Config, inviteSvc *inviteservice.Service, micTime
}
consumers = append(consumers, consumer)
}
if cfg.CPRelationshipWorker.Enabled && cfg.RocketMQ.RoomOutbox.Enabled {
// CP 关系消费使用独立 consumer group保证和麦位时长读模型各自拥有 MQ 位点和重试节奏。
consumer, err := rocketmqx.NewConsumer(cpRoomOutboxConsumerConfig(cfg))
if err != nil {
shutdownMQConsumers(consumers)
return nil, err
}
if err := consumer.Subscribe(cfg.RocketMQ.RoomOutbox.Topic, roommq.TagRoomOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
event, ok, err := roomGiftCPEventFromRoomMessage(message.Body)
if err != nil || !ok {
return err
}
_, err = cpSvc.ConsumeGiftEvent(appcode.WithContext(ctx, event.AppCode), event)
return err
}); err != nil {
_ = consumer.Shutdown()
shutdownMQConsumers(consumers)
return nil, err
}
consumers = append(consumers, consumer)
}
return consumers, nil
}
@ -623,6 +654,44 @@ func roomMicEventFromRoomMessage(body []byte) (mictimedomain.RoomMicEvent, bool,
}, true, nil
}
func roomGiftCPEventFromRoomMessage(body []byte) (cpdomain.GiftEvent, bool, error) {
envelope, _, err := roommq.DecodeRoomOutboxMessage(body)
if err != nil {
return cpdomain.GiftEvent{}, false, err
}
if envelope.GetEventType() != "RoomGiftSent" {
return cpdomain.GiftEvent{}, false, nil
}
var sent roomeventsv1.RoomGiftSent
if err := proto.Unmarshal(envelope.GetBody(), &sent); err != nil {
return cpdomain.GiftEvent{}, false, err
}
// 只有明确标记 cp_relation_type 的礼物才进入关系逻辑;普通礼物仍由其他读模型消费。
if strings.TrimSpace(sent.GetCpRelationType()) == "" {
return cpdomain.GiftEvent{}, false, nil
}
return cpdomain.GiftEvent{
AppCode: appcode.Normalize(envelope.GetAppCode()),
EventID: envelope.GetEventId(),
RoomID: envelope.GetRoomId(),
RoomVersion: envelope.GetRoomVersion(),
OccurredAtMS: envelope.GetOccurredAtMs(),
SenderUserID: sent.GetSenderUserId(),
TargetUserID: sent.GetTargetUserId(),
GiftID: sent.GetGiftId(),
GiftCount: sent.GetGiftCount(),
GiftValue: sent.GetGiftValue(),
BillingReceiptID: sent.GetBillingReceiptId(),
VisibleRegionID: sent.GetVisibleRegionId(),
CommandID: sent.GetCommandId(),
GiftTypeCode: sent.GetGiftTypeCode(),
CPRelationType: sent.GetCpRelationType(),
GiftName: sent.GetGiftName(),
GiftIconURL: sent.GetGiftIconUrl(),
GiftAnimationURL: sent.GetGiftAnimationUrl(),
}, true, nil
}
func rechargeTypeFromWalletPayload(payload string) string {
var decoded map[string]any
if err := json.Unmarshal([]byte(payload), &decoded); err == nil {
@ -659,6 +728,10 @@ func roomOutboxConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfi
return rocketmqConsumerConfig(cfg, cfg.RoomOutbox.ConsumerGroup, cfg.RoomOutbox.ConsumerMaxReconsumeTimes)
}
func cpRoomOutboxConsumerConfig(cfg config.Config) rocketmqx.ConsumerConfig {
return rocketmqConsumerConfig(cfg.RocketMQ, cfg.CPRelationshipWorker.ConsumerGroup, cfg.RocketMQ.RoomOutbox.ConsumerMaxReconsumeTimes)
}
func userOutboxProducerConfig(cfg config.RocketMQConfig) rocketmqx.ProducerConfig {
return rocketmqx.ProducerConfig{
EndpointConfig: rocketmqx.EndpointConfig{

View File

@ -38,6 +38,8 @@ type Config struct {
InviteRechargeWorker InviteRechargeWorkerConfig `yaml:"invite_recharge_worker"`
// MicTimeWorker 控制 room-service 麦位事件消费和用户麦上时长聚合。
MicTimeWorker MicTimeWorkerConfig `yaml:"mic_time_worker"`
// CPRelationshipWorker 控制 room-service 送礼事件消费和 CP 关系申请/亲密值维护。
CPRelationshipWorker CPRelationshipWorkerConfig `yaml:"cp_relationship_worker"`
// OutboxWorker 控制 user_outbox 到 MQ 的补偿投递。
OutboxWorker OutboxWorkerConfig `yaml:"outbox_worker"`
// RocketMQ 控制 wallet/room outbox MQ 消费。
@ -160,6 +162,14 @@ type MicTimeWorkerConfig struct {
Enabled bool `yaml:"enabled"`
}
// CPRelationshipWorkerConfig 保存 CP 礼物事件消费策略。
type CPRelationshipWorkerConfig struct {
// Enabled 控制是否启动 room gift MQ 消费;关闭后只能处理历史已消费关系。
Enabled bool `yaml:"enabled"`
// ConsumerGroup 必须独立于麦位消费,避免两个读模型共享 MQ 位点。
ConsumerGroup string `yaml:"consumer_group"`
}
// OutboxWorkerConfig 保存 user_outbox MQ 发布策略。
type OutboxWorkerConfig struct {
Enabled bool `yaml:"enabled"`
@ -247,6 +257,10 @@ func Default() Config {
MicTimeWorker: MicTimeWorkerConfig{
Enabled: false,
},
CPRelationshipWorker: CPRelationshipWorkerConfig{
Enabled: false,
ConsumerGroup: "hyapp-user-cp-room-outbox",
},
OutboxWorker: OutboxWorkerConfig{
Enabled: false,
PollInterval: time.Second,
@ -378,6 +392,13 @@ func Load(path string) (Config, error) {
if cfg.MicTimeWorker.Enabled && !cfg.RocketMQ.RoomOutbox.Enabled {
return Config{}, errors.New("mic_time_worker requires rocketmq.room_outbox.enabled")
}
cfg.CPRelationshipWorker.ConsumerGroup = strings.TrimSpace(cfg.CPRelationshipWorker.ConsumerGroup)
if cfg.CPRelationshipWorker.ConsumerGroup == "" {
cfg.CPRelationshipWorker.ConsumerGroup = "hyapp-user-cp-room-outbox"
}
if cfg.CPRelationshipWorker.Enabled && !cfg.RocketMQ.RoomOutbox.Enabled {
return Config{}, errors.New("cp_relationship_worker requires rocketmq.room_outbox.enabled")
}
if cfg.OutboxWorker.PollInterval <= 0 {
cfg.OutboxWorker.PollInterval = time.Second
}

View File

@ -65,6 +65,10 @@ type Session struct {
DeviceID string
// ExpiresAtMs 是 refresh session 过期时间。
ExpiresAtMs int64
// LastHeartbeatAtMs 是当前 App 会话最近一次被客户端心跳确认的时间。
LastHeartbeatAtMs int64
// LastHeartbeatRequestID 保存最近一次 App 心跳链路 ID方便按 request_id 反查客户端在线刷新。
LastHeartbeatRequestID string
// RevokedAtMs 非 0 表示会话已吊销。
RevokedAtMs int64
// RevokedReason 区分 logout、refresh 轮换、后台封禁和 IP 风控撤销。

View File

@ -0,0 +1,108 @@
// Package cp 定义 CP/兄弟/姐妹关系的领域事实和值对象。
package cp
const (
RelationTypeCP = "cp"
RelationTypeBrother = "brother"
RelationTypeSister = "sister"
ApplicationStatusPending = "pending"
ApplicationStatusAccepted = "accepted"
ApplicationStatusRejected = "rejected"
ApplicationStatusExpired = "expired"
ApplicationStatusBlocked = "blocked"
RelationshipStatusActive = "active"
RelationshipStatusEnded = "ended"
GiftTypeCodeCP = "cp"
EventTypeApplicationCreated = "UserCPApplicationCreated"
EventTypeApplicationAccepted = "UserCPApplicationAccepted"
EventTypeApplicationRejected = "UserCPApplicationRejected"
EventTypeRelationshipCreated = "UserCPRelationshipCreated"
EventTypeRelationshipIntimacyChanged = "UserCPRelationshipIntimacyChanged"
EventTypeRelationshipLevelUp = "UserCPRelationshipLevelUp"
)
// UserProfile 是 CP 申请和关系展示用的用户快照。
type UserProfile struct {
UserID int64
DisplayUserID string
Username string
Avatar string
}
// GiftSnapshot 固化触发申请或亲密值增长的礼物展示信息。
type GiftSnapshot struct {
GiftID string
GiftName string
GiftIconURL string
GiftAnimationURL string
GiftCount int32
GiftValue int64
BillingReceiptID string
}
// Application 表达一次关系申请,只有 target 用户可以处理 pending 状态。
type Application struct {
ApplicationID string
RelationType string
Status string
Requester UserProfile
Target UserProfile
RoomID string
RoomRegionID int64
Gift GiftSnapshot
CreatedAtMS int64
UpdatedAtMS int64
ExpiresAtMS int64
DecidedAtMS int64
SourceEventID string
SourceCommandID string
}
// Relationship 表达 A/B 已建立的一种 active 关系;展示时由 service 填充 me/partner。
type Relationship struct {
RelationshipID string
RelationType string
Status string
UserAID int64
UserBID int64
Me UserProfile
Partner UserProfile
IntimacyValue int64
Level int32
SourceApplicationID string
FormedAtMS int64
UpdatedAtMS int64
}
// GiftEvent 是 room outbox 中 RoomGiftSent 转换后的 CP 关系消费输入。
type GiftEvent struct {
AppCode string
EventID string
RoomID string
RoomVersion int64
OccurredAtMS int64
SenderUserID int64
TargetUserID int64
GiftID string
GiftCount int32
GiftValue int64
BillingReceiptID string
VisibleRegionID int64
CommandID string
GiftTypeCode string
CPRelationType string
GiftName string
GiftIconURL string
GiftAnimationURL string
}
// ConsumeResult 汇总一次送礼事件对 CP 领域产生的变化。
type ConsumeResult struct {
Consumed bool
Application Application
Relationship Relationship
}

View File

@ -56,6 +56,8 @@ type AuthRepository interface {
RevokeSession(ctx context.Context, sessionID string, refreshTokenHash string, revokedAtMs int64) (bool, error)
// RevokeSessionWithReason 按 session_id 幂等吊销 session并持久化撤销来源。
RevokeSessionWithReason(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error)
// UpdateSessionHeartbeat 刷新当前 App 登录会话心跳,必须校验 session 仍属于当前用户且未过期未吊销。
UpdateSessionHeartbeat(ctx context.Context, sessionID string, userID int64, heartbeatAtMs int64, requestID string) (authdomain.Session, error)
// FindThirdPartyIdentity 查找 provider + subject 的绑定。
FindThirdPartyIdentity(ctx context.Context, provider string, providerSubject string) (authdomain.ThirdPartyIdentity, error)
// FindUserIDByRegisterDeviceID 查找当前 App 下已经占用注册设备号的用户。

View File

@ -472,6 +472,47 @@ func TestIssueAccessTokenForSessionCarriesProfileRequiredOnboardingStatusWithout
}
}
func TestAppHeartbeatRefreshesCurrentSessionOnly(t *testing.T) {
// APP 心跳是登录会话在线状态,不应该创建房间 presence也不能续住其他用户或其他 session。
ctx := context.Background()
repository := mysqltest.NewRepository(t)
seedCountry(t, repository, "SG")
now := time.UnixMilli(1000)
authSvc := newAuthService(repository, &now, []int64{900011}, []string{"100011"})
token, _, err := authSvc.LoginThirdParty(ctx, "wechat", "openid-app-heartbeat", thirdPartyRegistration("ios"), authservice.Meta{AppCode: "lalu", RequestID: "req-login"})
if err != nil {
t.Fatalf("LoginThirdParty failed: %v", err)
}
now = time.UnixMilli(4000)
session, err := authSvc.AppHeartbeat(ctx, token.UserID, token.SessionID, authservice.Meta{AppCode: "lalu", RequestID: "req-app-heartbeat"})
if err != nil {
t.Fatalf("AppHeartbeat failed: %v", err)
}
if session.LastHeartbeatAtMs != 4000 || session.LastHeartbeatRequestID != "req-app-heartbeat" {
t.Fatalf("session heartbeat mismatch: %+v", session)
}
var heartbeatAtMS int64
var requestID string
if err := repository.RawDB().QueryRowContext(ctx, `
SELECT last_heartbeat_at_ms, last_heartbeat_request_id
FROM auth_sessions
WHERE app_code = 'lalu' AND session_id = ?
`, token.SessionID).Scan(&heartbeatAtMS, &requestID); err != nil {
t.Fatalf("query heartbeat failed: %v", err)
}
if heartbeatAtMS != 4000 || requestID != "req-app-heartbeat" {
t.Fatalf("stored heartbeat mismatch: at=%d request=%s", heartbeatAtMS, requestID)
}
now = time.UnixMilli(5000)
if _, err := authSvc.AppHeartbeat(ctx, token.UserID+1, token.SessionID, authservice.Meta{AppCode: "lalu", RequestID: "req-wrong-user"}); !xerr.IsCode(err, xerr.SessionRevoked) {
t.Fatalf("wrong user must not refresh session heartbeat, got %v", err)
}
}
func TestDisabledUserCannotLogin(t *testing.T) {
// 用户状态被禁用后,即使密码正确也不能继续签发 token。
ctx := context.Background()

View File

@ -140,6 +140,30 @@ func (s *Service) Logout(ctx context.Context, sessionID string, refreshToken str
return revoked, nil
}
// AppHeartbeat 刷新当前 App 登录会话的服务端在线时间。
func (s *Service) AppHeartbeat(ctx context.Context, userID int64, sessionID string, meta Meta) (authdomain.Session, error) {
sessionID = strings.TrimSpace(sessionID)
if userID <= 0 || sessionID == "" {
// APP 心跳只能续当前 access token 中的会话,不能让客户端传任意 user_id 或空 sid。
return authdomain.Session{}, xerr.New(xerr.InvalidArgument, "user_id and session_id are required")
}
if s.authRepository == nil {
return authdomain.Session{}, xerr.New(xerr.Unavailable, "auth repository is not configured")
}
nowMs := s.now().UnixMilli()
session, err := s.authRepository.UpdateSessionHeartbeat(ctx, sessionID, userID, nowMs, meta.RequestID)
if err != nil {
return authdomain.Session{}, err
}
if session.AppCode != appcode.Normalize(meta.AppCode) {
// 理论上 repository 已按 app_code 限定;这里保留防御,避免错误 context 造成跨 App 心跳成功。
return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
}
return session, nil
}
// TokenConfigValid 判断签发配置是否具备启动条件。
func (s *Service) TokenConfigValid() bool {
// 当前只支持 HS256避免签发和下游解析在算法上分叉。

View File

@ -0,0 +1,136 @@
// Package cp 承载 CP/兄弟/姐妹关系的用例编排。
package cp
import (
"context"
"strings"
"time"
"hyapp/pkg/xerr"
cpdomain "hyapp/services/user-service/internal/domain/cp"
)
// Repository 描述 CP 关系 service 需要的持久化能力,便于后续单元测试替换 MySQL 实现。
type Repository interface {
ListApplications(ctx context.Context, userID int64, direction string, status string, page int32, pageSize int32, nowMs int64) ([]cpdomain.Application, int64, error)
AcceptApplication(ctx context.Context, userID int64, applicationID string, nowMs int64) (cpdomain.Application, cpdomain.Relationship, error)
RejectApplication(ctx context.Context, userID int64, applicationID string, reason string, nowMs int64) (cpdomain.Application, error)
ListRelationships(ctx context.Context, userID int64, relationType string, page int32, pageSize int32) ([]cpdomain.Relationship, int64, error)
ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent, nowMs int64) (cpdomain.ConsumeResult, error)
}
// Service 只编排 CP 用例,不直接投递 IMIM 由 user_outbox 下游消费者按事实事件处理。
type Service struct {
repo Repository
clock func() time.Time
}
// New 创建 CP 关系 service。
func New(repo Repository) *Service {
return &Service{
repo: repo,
clock: func() time.Time { return time.Now().UTC() },
}
}
// ListApplications 返回用户收到或发出的 CP 申请;过期固化在 repository 中完成。
func (s *Service) ListApplications(ctx context.Context, userID int64, direction string, status string, page int32, pageSize int32) ([]cpdomain.Application, int64, int64, error) {
if err := requireUserID(userID); err != nil {
return nil, 0, 0, err
}
nowMs := s.nowMs()
applications, total, err := s.repo.ListApplications(ctx, userID, normalizeDirection(direction), normalizeStatus(status), page, pageSize, nowMs)
return applications, total, nowMs, err
}
// AcceptApplication 同意申请并建立关系;同一用户只能有一个 active 关系的互斥由 repository 事务保证。
func (s *Service) AcceptApplication(ctx context.Context, userID int64, applicationID string) (cpdomain.Application, cpdomain.Relationship, error) {
if err := requireUserID(userID); err != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
if strings.TrimSpace(applicationID) == "" {
return cpdomain.Application{}, cpdomain.Relationship{}, xerr.New(xerr.InvalidArgument, "application_id is required")
}
return s.repo.AcceptApplication(ctx, userID, strings.TrimSpace(applicationID), s.nowMs())
}
// RejectApplication 拒绝申请;拒绝不阻断同一对用户其他类型的 pending 申请。
func (s *Service) RejectApplication(ctx context.Context, userID int64, applicationID string, reason string) (cpdomain.Application, error) {
if err := requireUserID(userID); err != nil {
return cpdomain.Application{}, err
}
if strings.TrimSpace(applicationID) == "" {
return cpdomain.Application{}, xerr.New(xerr.InvalidArgument, "application_id is required")
}
return s.repo.RejectApplication(ctx, userID, strings.TrimSpace(applicationID), reason, s.nowMs())
}
// ListRelationships 返回用户当前 active 关系;当前产品固定每个用户同时只能有一条关系。
func (s *Service) ListRelationships(ctx context.Context, userID int64, relationType string, page int32, pageSize int32) ([]cpdomain.Relationship, int64, error) {
if err := requireUserID(userID); err != nil {
return nil, 0, err
}
return s.repo.ListRelationships(ctx, userID, normalizeRelationType(relationType), page, pageSize)
}
// ConsumeGiftEvent 消费 room-service 已扣费并落房间状态的送礼事实。
func (s *Service) ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent) (cpdomain.ConsumeResult, error) {
event.CPRelationType = normalizeRelationType(event.CPRelationType)
if event.CPRelationType == "" {
return cpdomain.ConsumeResult{Consumed: false}, nil
}
// gift_value 是亲密值增量,沿用 room 热度结算后的值0 值仍可创建申请,但不会增加已有关联亲密值。
if event.GiftValue < 0 {
event.GiftValue = 0
}
return s.repo.ConsumeGiftEvent(ctx, event, s.nowMs())
}
func (s *Service) nowMs() int64 {
if s == nil || s.clock == nil {
return time.Now().UTC().UnixMilli()
}
return s.clock().UTC().UnixMilli()
}
func requireUserID(userID int64) error {
if userID <= 0 {
return xerr.New(xerr.InvalidArgument, "user_id is required")
}
return nil
}
func normalizeDirection(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "outgoing":
return "outgoing"
default:
return "incoming"
}
}
func normalizeStatus(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case cpdomain.ApplicationStatusPending,
cpdomain.ApplicationStatusAccepted,
cpdomain.ApplicationStatusRejected,
cpdomain.ApplicationStatusExpired,
cpdomain.ApplicationStatusBlocked:
return strings.ToLower(strings.TrimSpace(value))
default:
return ""
}
}
func normalizeRelationType(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case cpdomain.RelationTypeCP:
return cpdomain.RelationTypeCP
case cpdomain.RelationTypeBrother:
return cpdomain.RelationTypeBrother
case cpdomain.RelationTypeSister:
return cpdomain.RelationTypeSister
default:
return ""
}
}

View File

@ -12,9 +12,9 @@ import (
func (r *Repository) CreateSession(ctx context.Context, session authdomain.Session) error {
// refresh token 只保存 hash原文不会写入数据库。
_, err := r.db.ExecContext(ctx, `
INSERT INTO auth_sessions (app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?, NULL, '', '', '', ?, ?)
`, appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.RefreshTokenHash, session.DeviceID, session.ExpiresAtMs, session.CreatedAtMs, session.UpdatedAtMs)
INSERT INTO auth_sessions (app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, last_heartbeat_at_ms, last_heartbeat_request_id, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, '', '', '', ?, ?)
`, appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.RefreshTokenHash, session.DeviceID, session.ExpiresAtMs, session.LastHeartbeatAtMs, session.LastHeartbeatRequestID, session.CreatedAtMs, session.UpdatedAtMs)
if err != nil {
// session_id 或 refresh hash 冲突统一映射成 auth 领域冲突。
return mapAuthDuplicateError(err)
@ -124,6 +124,28 @@ func (r *Repository) RevokeSessionWithReason(ctx context.Context, sessionID stri
return r.revokeSessionByID(ctx, sessionID, revokedAtMs, reason, requestID, revokedBy)
}
func (r *Repository) UpdateSessionHeartbeat(ctx context.Context, sessionID string, userID int64, heartbeatAtMs int64, requestID string) (authdomain.Session, error) {
// App 心跳绑定服务端 session不能只按 user_id 宽泛刷新,避免旧 token 或跨设备请求续住错误会话。
result, err := r.db.ExecContext(ctx, `
UPDATE auth_sessions
SET last_heartbeat_at_ms = ?, last_heartbeat_request_id = ?, updated_at_ms = ?
WHERE app_code = ? AND session_id = ? AND user_id = ? AND revoked_at_ms IS NULL AND expires_at_ms > ?
`, heartbeatAtMs, requestID, heartbeatAtMs, appcode.FromContext(ctx), sessionID, userID, heartbeatAtMs)
if err != nil {
return authdomain.Session{}, err
}
affected, err := result.RowsAffected()
if err != nil {
return authdomain.Session{}, err
}
if affected == 0 {
// 未命中只说明 session 不可用或不属于当前用户;对外统一成 session revoked避免泄漏会话存在性。
return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
}
return r.findSessionByID(ctx, sessionID)
}
func (r *Repository) revokeSessionByID(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error) {
result, err := r.db.ExecContext(ctx, `
UPDATE auth_sessions
@ -150,10 +172,10 @@ func (r *Repository) findSessionByRefreshHash(ctx context.Context, refreshTokenH
var session authdomain.Session
var revokedAt sql.Null[int64]
err := r.db.QueryRowContext(ctx, `
SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, last_heartbeat_at_ms, last_heartbeat_request_id, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
FROM auth_sessions
WHERE app_code = ? AND refresh_token_hash = ?
`, appcode.FromContext(ctx), refreshTokenHash).Scan(&session.AppCode, &session.SessionID, &session.UserID, &session.RefreshTokenHash, &session.DeviceID, &session.ExpiresAtMs, &revokedAt, &session.RevokedReason, &session.RevokedRequestID, &session.RevokedBy, &session.CreatedAtMs, &session.UpdatedAtMs)
`, appcode.FromContext(ctx), refreshTokenHash).Scan(&session.AppCode, &session.SessionID, &session.UserID, &session.RefreshTokenHash, &session.DeviceID, &session.ExpiresAtMs, &session.LastHeartbeatAtMs, &session.LastHeartbeatRequestID, &revokedAt, &session.RevokedReason, &session.RevokedRequestID, &session.RevokedBy, &session.CreatedAtMs, &session.UpdatedAtMs)
if err == sql.ErrNoRows {
// 找不到 hash 按 revoked 返回,避免暴露 token 是否存在。
return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
@ -174,10 +196,10 @@ func (r *Repository) findSessionByID(ctx context.Context, sessionID string) (aut
var session authdomain.Session
var revokedAt sql.Null[int64]
err := r.db.QueryRowContext(ctx, `
SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, last_heartbeat_at_ms, last_heartbeat_request_id, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
FROM auth_sessions
WHERE app_code = ? AND session_id = ?
`, appcode.FromContext(ctx), sessionID).Scan(&session.AppCode, &session.SessionID, &session.UserID, &session.RefreshTokenHash, &session.DeviceID, &session.ExpiresAtMs, &revokedAt, &session.RevokedReason, &session.RevokedRequestID, &session.RevokedBy, &session.CreatedAtMs, &session.UpdatedAtMs)
`, appcode.FromContext(ctx), sessionID).Scan(&session.AppCode, &session.SessionID, &session.UserID, &session.RefreshTokenHash, &session.DeviceID, &session.ExpiresAtMs, &session.LastHeartbeatAtMs, &session.LastHeartbeatRequestID, &revokedAt, &session.RevokedReason, &session.RevokedRequestID, &session.RevokedBy, &session.CreatedAtMs, &session.UpdatedAtMs)
if err == sql.ErrNoRows {
// 不暴露 session_id 是否存在,统一按 revoked 处理。
return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
@ -195,9 +217,9 @@ func (r *Repository) findSessionByID(ctx context.Context, sessionID string) (aut
func insertSession(ctx context.Context, tx *sql.Tx, session authdomain.Session) error {
// insertSession 只在已有事务中使用,保证与 session 替换或三方注册同提交。
_, err := tx.ExecContext(ctx, `
INSERT INTO auth_sessions (app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?, NULL, '', '', '', ?, ?)
`, appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.RefreshTokenHash, session.DeviceID, session.ExpiresAtMs, session.CreatedAtMs, session.UpdatedAtMs)
INSERT INTO auth_sessions (app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, last_heartbeat_at_ms, last_heartbeat_request_id, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, '', '', '', ?, ?)
`, appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.RefreshTokenHash, session.DeviceID, session.ExpiresAtMs, session.LastHeartbeatAtMs, session.LastHeartbeatRequestID, session.CreatedAtMs, session.UpdatedAtMs)
if err != nil {
return mapAuthDuplicateError(err)
}

View File

@ -0,0 +1,808 @@
// Package cp 持久化 CP/兄弟/姐妹申请、单 active 关系、亲密值和等级默认规则。
package cp
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
cpdomain "hyapp/services/user-service/internal/domain/cp"
)
// Repository 是 CP 关系在 user-service MySQL 上的 owner 存储。
type Repository struct {
db *sql.DB
}
// New 基于共享 MySQL 连接池创建 CP 关系存储。
func New(db *sql.DB) *Repository {
return &Repository{db: db}
}
// ListApplications 按用户和方向读取申请,同时懒过期已超时的 pending 申请。
func (r *Repository) ListApplications(ctx context.Context, userID int64, direction string, status string, page int32, pageSize int32, nowMs int64) ([]cpdomain.Application, int64, error) {
if r == nil || r.db == nil {
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
appCode := appcode.FromContext(ctx)
// 列表读取前先做一次轻量懒过期,避免客户端看到已经超过 24 小时但仍可操作的 pending 申请。
if _, err := r.db.ExecContext(ctx, `
UPDATE user_cp_applications
SET status = ?, updated_at_ms = ?
WHERE app_code = ? AND status = ? AND expires_at_ms <= ?`,
cpdomain.ApplicationStatusExpired, nowMs, appCode, cpdomain.ApplicationStatusPending, nowMs,
); err != nil {
return nil, 0, err
}
where := []string{"a.app_code = ?"}
args := []any{appCode}
// direction 只影响 requester/target 过滤,不把双向申请合并,客户端才能区分我发出的和我收到的申请。
switch strings.ToLower(strings.TrimSpace(direction)) {
case "outgoing":
where = append(where, "a.requester_user_id = ?")
args = append(args, userID)
default:
where = append(where, "a.target_user_id = ?")
args = append(args, userID)
}
if status = strings.ToLower(strings.TrimSpace(status)); status != "" {
where = append(where, "a.status = ?")
args = append(args, status)
}
total, err := countRows(ctx, r.db, "SELECT COUNT(*) FROM user_cp_applications a WHERE "+strings.Join(where, " AND "), args...)
if err != nil {
return nil, 0, err
}
args = append(args, clampPageSize(pageSize), offset(page, pageSize))
rows, err := r.db.QueryContext(ctx, applicationSelectSQL("WHERE "+strings.Join(where, " AND ")+`
ORDER BY a.created_at_ms DESC, a.application_id DESC
LIMIT ? OFFSET ?`), args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
applications, err := scanApplications(rows)
if err != nil {
return nil, 0, err
}
return applications, total, nil
}
// AcceptApplication 处理同意申请,事务内保证同一对用户只能形成一个 active 关系,并按关系类型容量限制个人数量。
func (r *Repository) AcceptApplication(ctx context.Context, userID int64, applicationID string, nowMs int64) (cpdomain.Application, cpdomain.Relationship, error) {
if r == nil || r.db == nil {
return cpdomain.Application{}, cpdomain.Relationship{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
defer func() { _ = tx.Rollback() }()
appCode := appcode.FromContext(ctx)
if err := ensureDefaultConfigTx(ctx, tx, appCode, nowMs); err != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
// 先锁申请行,再判断状态和过期时间,保证两个端同时点同意/拒绝时只有一个事务能改状态。
application, err := lockApplicationTx(ctx, tx, appCode, applicationID)
if err != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
if application.Target.UserID != userID {
return cpdomain.Application{}, cpdomain.Relationship{}, xerr.New(xerr.PermissionDenied, "only target user can accept cp application")
}
if err := assertPendingApplication(application, nowMs); err != nil {
if isExpiredPendingApplication(application, nowMs) {
// 过期申请虽然不能继续同意,但 expired 本身是一个需要持久化的业务事实;
// 这里必须在返回冲突前提交事务,否则 defer Rollback 会把状态收敛一起回滚掉。
if markErr := markApplicationExpiredTx(ctx, tx, appCode, applicationID, nowMs); markErr != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, markErr
}
if commitErr := tx.Commit(); commitErr != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, commitErr
}
}
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
userAID, userBID := orderedPair(application.Requester.UserID, application.Target.UserID)
// A/B 关系按无序 pair 加锁;不论 CP、兄弟还是姐妹同一时间只能存在一种 active 关系。
existing, found, err := lockActiveRelationshipTx(ctx, tx, appCode, userAID, userBID)
if err != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
if found {
return cpdomain.Application{}, cpdomain.Relationship{}, xerr.New(xerr.Conflict, fmt.Sprintf("%s用户和你已经有%s关系", application.Requester.Username, existing.RelationType))
}
// 容量校验按关系类型读取后台配置CP 固定只能有一个,兄弟/姐妹允许按运营配置扩展。
if err := assertRelationCapacityTx(ctx, tx, appCode, application.RelationType, application.Requester.UserID, application.Target.UserID); err != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
relationshipID := relationshipIDForPair(userAID, userBID)
if _, err := tx.ExecContext(ctx, `
INSERT INTO user_cp_relationships (
app_code, relationship_id, user_a_id, user_b_id, relation_type, status,
intimacy_value, level_no, source_application_id,
formed_at_ms, ended_at_ms, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, 0, 1, ?, ?, 0, ?, ?)`,
appCode, relationshipID, userAID, userBID, application.RelationType, cpdomain.RelationshipStatusActive,
application.ApplicationID, nowMs, nowMs, nowMs,
); err != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
// 同意一种关系后,同一对用户之间其他类型的 pending 申请不再允许接受,状态置为 blocked 便于客户端提示。
if _, err := tx.ExecContext(ctx, `
UPDATE user_cp_applications
SET status = ?, decided_at_ms = ?, updated_at_ms = ?
WHERE app_code = ? AND application_id = ?`,
cpdomain.ApplicationStatusAccepted, nowMs, nowMs, appCode, application.ApplicationID,
); err != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
if _, err := tx.ExecContext(ctx, `
UPDATE user_cp_applications
SET status = ?, updated_at_ms = ?
WHERE app_code = ?
AND status = ?
AND requester_user_id IN (?, ?)
AND target_user_id IN (?, ?)
AND application_id <> ?`,
cpdomain.ApplicationStatusBlocked, nowMs, appCode, cpdomain.ApplicationStatusPending,
application.Requester.UserID, application.Target.UserID, application.Requester.UserID, application.Target.UserID, application.ApplicationID,
); err != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
application.Status = cpdomain.ApplicationStatusAccepted
application.UpdatedAtMS = nowMs
application.DecidedAtMS = nowMs
relationship, err := queryRelationshipByIDTx(ctx, tx, appCode, userID, relationshipID)
if err != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
// user_outbox 只记录事实IM、区域广播和后续 push 由下游 notice/统计等消费者按事件类型各自投递。
if err := insertOutboxTx(ctx, tx, appCode, cpdomain.EventTypeApplicationAccepted, "cp_application", application.Target.UserID, application, nowMs); err != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
relationshipPayload := map[string]any{"application": application, "relationship": relationship}
if err := insertOutboxTx(ctx, tx, appCode, cpdomain.EventTypeRelationshipCreated, "cp_relationship", application.Target.UserID, relationshipPayload, nowMs); err != nil {
return cpdomain.Application{}, cpdomain.Relationship{}, err
}
return application, relationship, tx.Commit()
}
// RejectApplication 处理拒绝申请;拒绝只改变该申请,不影响同一对用户其他 pending 类型。
func (r *Repository) RejectApplication(ctx context.Context, userID int64, applicationID string, reason string, nowMs int64) (cpdomain.Application, error) {
if r == nil || r.db == nil {
return cpdomain.Application{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return cpdomain.Application{}, err
}
defer func() { _ = tx.Rollback() }()
appCode := appcode.FromContext(ctx)
application, err := lockApplicationTx(ctx, tx, appCode, applicationID)
if err != nil {
return cpdomain.Application{}, err
}
if application.Target.UserID != userID {
return cpdomain.Application{}, xerr.New(xerr.PermissionDenied, "only target user can reject cp application")
}
if err := assertPendingApplication(application, nowMs); err != nil {
if isExpiredPendingApplication(application, nowMs) {
// 拒绝接口同样承担惰性过期收敛;过期事实要先提交,再把“不能操作”的冲突返回给客户端。
if markErr := markApplicationExpiredTx(ctx, tx, appCode, applicationID, nowMs); markErr != nil {
return cpdomain.Application{}, markErr
}
if commitErr := tx.Commit(); commitErr != nil {
return cpdomain.Application{}, commitErr
}
}
return cpdomain.Application{}, err
}
if _, err := tx.ExecContext(ctx, `
UPDATE user_cp_applications
SET status = ?, decided_at_ms = ?, updated_at_ms = ?
WHERE app_code = ? AND application_id = ?`,
cpdomain.ApplicationStatusRejected, nowMs, nowMs, appCode, application.ApplicationID,
); err != nil {
return cpdomain.Application{}, err
}
application.Status = cpdomain.ApplicationStatusRejected
application.UpdatedAtMS = nowMs
application.DecidedAtMS = nowMs
payload := map[string]any{"application": application, "reason": strings.TrimSpace(reason)}
if err := insertOutboxTx(ctx, tx, appCode, cpdomain.EventTypeApplicationRejected, "cp_application", application.Target.UserID, payload, nowMs); err != nil {
return cpdomain.Application{}, err
}
return application, tx.Commit()
}
// ListRelationships 读取用户 active 关系列表CP 最多一条,兄弟/姐妹可能多条,因此分页必须保留。
func (r *Repository) ListRelationships(ctx context.Context, userID int64, relationType string, page int32, pageSize int32) ([]cpdomain.Relationship, int64, error) {
if r == nil || r.db == nil {
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
appCode := appcode.FromContext(ctx)
where := []string{"r.app_code = ?", "r.status = ?", "(r.user_a_id = ? OR r.user_b_id = ?)"}
args := []any{appCode, cpdomain.RelationshipStatusActive, userID, userID}
if relationType = normalizeRelationType(relationType); relationType != "" {
where = append(where, "r.relation_type = ?")
args = append(args, relationType)
}
// total 只统计 active 关系;历史关系不会占用当前关系数量,也不会在 App 资料卡展示。
total, err := countRows(ctx, r.db, "SELECT COUNT(*) FROM user_cp_relationships r WHERE "+strings.Join(where, " AND "), args...)
if err != nil {
return nil, 0, err
}
// relationshipSelectSQL 的前两个占位是 viewer/me 和 partner 选择suffix 的过滤参数必须跟在它们后面。
queryArgs := []any{userID, userID}
queryArgs = append(queryArgs, args...)
queryArgs = append(queryArgs, clampPageSize(pageSize), offset(page, pageSize))
rows, err := r.db.QueryContext(ctx, relationshipSelectSQL("WHERE "+strings.Join(where, " AND ")+`
ORDER BY r.formed_at_ms DESC, r.relationship_id DESC
LIMIT ? OFFSET ?`), queryArgs...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
relationships, err := scanRelationships(rows)
if err != nil {
return nil, 0, err
}
return relationships, total, nil
}
// ConsumeGiftEvent 把房间送礼事实转换为 CP 申请或亲密值增长。
func (r *Repository) ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent, nowMs int64) (cpdomain.ConsumeResult, error) {
if r == nil || r.db == nil {
return cpdomain.ConsumeResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
relationType := normalizeRelationType(event.CPRelationType)
if relationType == "" || event.SenderUserID <= 0 || event.TargetUserID <= 0 || event.SenderUserID == event.TargetUserID {
return cpdomain.ConsumeResult{Consumed: false}, nil
}
if strings.TrimSpace(event.EventID) == "" {
return cpdomain.ConsumeResult{}, xerr.New(xerr.InvalidArgument, "room event id is required")
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return cpdomain.ConsumeResult{}, err
}
defer func() { _ = tx.Rollback() }()
appCode := appcode.FromContext(ctx)
if event.AppCode != "" {
appCode = appcode.Normalize(event.AppCode)
}
if err := ensureDefaultConfigTx(ctx, tx, appCode, nowMs); err != nil {
return cpdomain.ConsumeResult{}, err
}
// room outbox event_id 是消费幂等键;重复投递时直接提交空事务,避免重复创建申请或重复加亲密值。
if exists, err := lockConsumedEventTx(ctx, tx, appCode, event.EventID); err != nil {
return cpdomain.ConsumeResult{}, err
} else if exists {
return cpdomain.ConsumeResult{Consumed: false}, tx.Commit()
}
userAID, userBID := orderedPair(event.SenderUserID, event.TargetUserID)
// 已有 active 关系时,任何关系礼物都只增加这对关系的亲密值,不再生成新的申请。
if relationship, found, err := lockActiveRelationshipTx(ctx, tx, appCode, userAID, userBID); err != nil {
return cpdomain.ConsumeResult{}, err
} else if found {
updated, err := addRelationshipIntimacyTx(ctx, tx, appCode, relationship, event.GiftValue, event.TargetUserID, nowMs)
if err != nil {
return cpdomain.ConsumeResult{}, err
}
if err := insertConsumptionTx(ctx, tx, appCode, event.EventID, "relationship_intimacy_added", "", updated.RelationshipID, "", nowMs); err != nil {
return cpdomain.ConsumeResult{}, err
}
if err := insertOutboxTx(ctx, tx, appCode, cpdomain.EventTypeRelationshipIntimacyChanged, "cp_relationship", event.TargetUserID, map[string]any{"relationship": updated, "gift_event": event}, nowMs); err != nil {
return cpdomain.ConsumeResult{}, err
}
return cpdomain.ConsumeResult{Consumed: true, Relationship: updated}, tx.Commit()
}
// 没有 active 关系时CP/兄弟/姐妹礼物只生成 pending 申请;真正建立关系必须由 B 显式同意。
if err := ensurePairHasNoActiveRelationshipTx(ctx, tx, appCode, userAID, userBID, event.TargetUserID); err != nil {
if err := insertConsumptionTx(ctx, tx, appCode, event.EventID, "skipped", "", "", "existing_relationship", nowMs); err != nil {
return cpdomain.ConsumeResult{}, err
}
return cpdomain.ConsumeResult{Consumed: true}, tx.Commit()
}
application, err := upsertApplicationFromGiftTx(ctx, tx, appCode, event, relationType, nowMs)
if err != nil {
return cpdomain.ConsumeResult{}, err
}
if err := insertConsumptionTx(ctx, tx, appCode, event.EventID, "application_created", application.ApplicationID, "", "", nowMs); err != nil {
return cpdomain.ConsumeResult{}, err
}
if err := insertOutboxTx(ctx, tx, appCode, cpdomain.EventTypeApplicationCreated, "cp_application", application.Target.UserID, application, nowMs); err != nil {
return cpdomain.ConsumeResult{}, err
}
return cpdomain.ConsumeResult{Consumed: true, Application: application}, tx.Commit()
}
func ensureDefaultConfigTx(ctx context.Context, tx *sql.Tx, appCode string, nowMs int64) error {
// 默认值只兜底本地和新 App 启动CP 固定 1 个,兄弟/姐妹默认 5 个并允许后台覆盖。
for _, relationType := range []string{cpdomain.RelationTypeCP, cpdomain.RelationTypeBrother, cpdomain.RelationTypeSister} {
maxCount := int32(1)
if relationType == cpdomain.RelationTypeBrother || relationType == cpdomain.RelationTypeSister {
maxCount = 5
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO user_cp_relation_configs (
app_code, relation_type, max_count_per_user, application_expire_hours,
status, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, 24, 'active', ?, ?)
ON DUPLICATE KEY UPDATE relation_type = VALUES(relation_type)`,
appCode, relationType, maxCount, nowMs, nowMs,
); err != nil {
return err
}
for level := int32(1); level <= 5; level++ {
threshold := int64(level-1) * 10000
if _, err := tx.ExecContext(ctx, `
INSERT INTO user_cp_level_rules (
app_code, relation_type, level_no, intimacy_threshold,
reward_resource_group_id, level_icon_url, status, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, 0, '', 'active', ?, ?)
ON DUPLICATE KEY UPDATE level_no = VALUES(level_no)`,
appCode, relationType, level, threshold, nowMs, nowMs,
); err != nil {
return err
}
}
}
return nil
}
func applicationSelectSQL(suffix string) string {
// 申请查询统一 join 双方 users 快照,避免 service 层为每条申请再发起批量用户查询。
return `
SELECT
a.application_id, a.relation_type, a.status,
a.room_id, a.room_region_id,
a.gift_id, a.gift_name, a.gift_icon_url, a.gift_animation_url,
a.gift_count, a.gift_value, a.billing_receipt_id,
a.source_room_event_id, a.source_command_id,
a.created_at_ms, a.updated_at_ms, a.expires_at_ms, a.decided_at_ms,
requester.user_id, COALESCE(requester.current_display_user_id, ''), COALESCE(requester.username, ''), COALESCE(requester.avatar, ''),
target.user_id, COALESCE(target.current_display_user_id, ''), COALESCE(target.username, ''), COALESCE(target.avatar, '')
FROM user_cp_applications a
JOIN users requester ON requester.app_code = a.app_code AND requester.user_id = a.requester_user_id
JOIN users target ON target.app_code = a.app_code AND target.user_id = a.target_user_id
` + suffix
}
func scanApplications(rows *sql.Rows) ([]cpdomain.Application, error) {
applications := make([]cpdomain.Application, 0)
for rows.Next() {
application, err := scanApplication(rows)
if err != nil {
return nil, err
}
applications = append(applications, application)
}
return applications, rows.Err()
}
func scanApplication(scanner interface{ Scan(dest ...any) error }) (cpdomain.Application, error) {
var app cpdomain.Application
err := scanner.Scan(
&app.ApplicationID, &app.RelationType, &app.Status,
&app.RoomID, &app.RoomRegionID,
&app.Gift.GiftID, &app.Gift.GiftName, &app.Gift.GiftIconURL, &app.Gift.GiftAnimationURL,
&app.Gift.GiftCount, &app.Gift.GiftValue, &app.Gift.BillingReceiptID,
&app.SourceEventID, &app.SourceCommandID,
&app.CreatedAtMS, &app.UpdatedAtMS, &app.ExpiresAtMS, &app.DecidedAtMS,
&app.Requester.UserID, &app.Requester.DisplayUserID, &app.Requester.Username, &app.Requester.Avatar,
&app.Target.UserID, &app.Target.DisplayUserID, &app.Target.Username, &app.Target.Avatar,
)
return app, err
}
func lockApplicationTx(ctx context.Context, tx *sql.Tx, appCode string, applicationID string) (cpdomain.Application, error) {
row := tx.QueryRowContext(ctx, applicationSelectSQL(`
WHERE a.app_code = ? AND a.application_id = ?
FOR UPDATE`), appCode, strings.TrimSpace(applicationID))
application, err := scanApplication(row)
if err == sql.ErrNoRows {
return cpdomain.Application{}, xerr.New(xerr.NotFound, "cp application not found")
}
return application, err
}
func markApplicationExpiredTx(ctx context.Context, tx *sql.Tx, appCode string, applicationID string, nowMs int64) error {
_, err := tx.ExecContext(ctx, `
UPDATE user_cp_applications
SET status = ?, updated_at_ms = ?
WHERE app_code = ? AND application_id = ? AND status = ?`,
cpdomain.ApplicationStatusExpired, nowMs, appCode, applicationID, cpdomain.ApplicationStatusPending,
)
return err
}
func isExpiredPendingApplication(application cpdomain.Application, nowMs int64) bool {
// 只有 pending 且超过过期时间的申请才做惰性状态收敛accepted/rejected/blocked 等终态不能被误改成 expired。
return application.Status == cpdomain.ApplicationStatusPending && application.ExpiresAtMS <= nowMs
}
func assertPendingApplication(application cpdomain.Application, nowMs int64) error {
if application.Status != cpdomain.ApplicationStatusPending {
return xerr.New(xerr.Conflict, "cp application is not pending")
}
if application.ExpiresAtMS <= nowMs {
return xerr.New(xerr.Conflict, "cp application expired")
}
return nil
}
func relationshipSelectSQL(suffix string) string {
// viewerUserID 连续出现两次me 投影和 partner 选择都必须以当前查看用户为准。
return `
SELECT
r.relationship_id, r.relation_type, r.status, r.user_a_id, r.user_b_id,
r.intimacy_value, r.level_no, r.source_application_id, r.formed_at_ms, r.updated_at_ms,
me.user_id, COALESCE(me.current_display_user_id, ''), COALESCE(me.username, ''), COALESCE(me.avatar, ''),
partner.user_id, COALESCE(partner.current_display_user_id, ''), COALESCE(partner.username, ''), COALESCE(partner.avatar, '')
FROM user_cp_relationships r
JOIN users me ON me.app_code = r.app_code AND me.user_id = ?
JOIN users partner ON partner.app_code = r.app_code
AND partner.user_id = CASE WHEN r.user_a_id = ? THEN r.user_b_id ELSE r.user_a_id END
` + suffix
}
func scanRelationships(rows *sql.Rows) ([]cpdomain.Relationship, error) {
relationships := make([]cpdomain.Relationship, 0)
for rows.Next() {
relationship, err := scanRelationship(rows)
if err != nil {
return nil, err
}
relationships = append(relationships, relationship)
}
return relationships, rows.Err()
}
func scanRelationship(scanner interface{ Scan(dest ...any) error }) (cpdomain.Relationship, error) {
var relationship cpdomain.Relationship
err := scanner.Scan(
&relationship.RelationshipID, &relationship.RelationType, &relationship.Status, &relationship.UserAID, &relationship.UserBID,
&relationship.IntimacyValue, &relationship.Level, &relationship.SourceApplicationID, &relationship.FormedAtMS, &relationship.UpdatedAtMS,
&relationship.Me.UserID, &relationship.Me.DisplayUserID, &relationship.Me.Username, &relationship.Me.Avatar,
&relationship.Partner.UserID, &relationship.Partner.DisplayUserID, &relationship.Partner.Username, &relationship.Partner.Avatar,
)
return relationship, err
}
func queryRelationshipByIDTx(ctx context.Context, tx *sql.Tx, appCode string, viewerUserID int64, relationshipID string) (cpdomain.Relationship, error) {
row := tx.QueryRowContext(ctx, relationshipSelectSQL(`
WHERE r.app_code = ? AND r.relationship_id = ?`),
viewerUserID, viewerUserID, appCode, strings.TrimSpace(relationshipID),
)
relationship, err := scanRelationship(row)
if err == sql.ErrNoRows {
return cpdomain.Relationship{}, xerr.New(xerr.NotFound, "cp relationship not found")
}
return relationship, err
}
func lockActiveRelationshipTx(ctx context.Context, tx *sql.Tx, appCode string, userAID int64, userBID int64) (cpdomain.Relationship, bool, error) {
// active 关系行用 FOR UPDATE 锁住 pair关系创建和送礼加亲密值都走这把锁。
row := tx.QueryRowContext(ctx, `
SELECT relationship_id, relation_type, status, user_a_id, user_b_id,
intimacy_value, level_no, source_application_id, formed_at_ms, updated_at_ms
FROM user_cp_relationships
WHERE app_code = ? AND user_a_id = ? AND user_b_id = ? AND status = ?
FOR UPDATE`,
appCode, userAID, userBID, cpdomain.RelationshipStatusActive,
)
var relationship cpdomain.Relationship
err := row.Scan(
&relationship.RelationshipID, &relationship.RelationType, &relationship.Status, &relationship.UserAID, &relationship.UserBID,
&relationship.IntimacyValue, &relationship.Level, &relationship.SourceApplicationID, &relationship.FormedAtMS, &relationship.UpdatedAtMS,
)
if err == sql.ErrNoRows {
return cpdomain.Relationship{}, false, nil
}
return relationship, true, err
}
func ensurePairHasNoActiveRelationshipTx(ctx context.Context, tx *sql.Tx, appCode string, userAID int64, userBID int64, viewerUserID int64) error {
if relationship, found, err := lockActiveRelationshipTx(ctx, tx, appCode, userAID, userBID); err != nil {
return err
} else if found {
partnerID := userAID
if partnerID == viewerUserID {
partnerID = userBID
}
return xerr.New(xerr.Conflict, fmt.Sprintf("%d用户和你已经有%s关系", partnerID, relationship.RelationType))
}
return nil
}
func assertRelationCapacityTx(ctx context.Context, tx *sql.Tx, appCode string, relationType string, userAID int64, userBID int64) error {
if normalizeRelationType(relationType) == "" {
return xerr.New(xerr.Conflict, "cp relation type is disabled")
}
maxCount, err := relationMaxCountTx(ctx, tx, appCode, relationType)
if err != nil {
return err
}
for _, userID := range []int64{userAID, userBID} {
// 容量按用户和关系类型计算;同一对用户互斥由 pair 锁保证,不同对象可以同时拥有兄弟/姐妹关系。
total, err := countRows(ctx, tx, `
SELECT COUNT(*)
FROM user_cp_relationships
WHERE app_code = ? AND status = ?
AND relation_type = ?
AND (user_a_id = ? OR user_b_id = ?)`,
appCode, cpdomain.RelationshipStatusActive, relationType, userID, userID,
)
if err != nil {
return err
}
if total >= int64(maxCount) {
return xerr.New(xerr.Conflict, "user cp relation count reaches limit")
}
}
return nil
}
func relationMaxCountTx(ctx context.Context, tx *sql.Tx, appCode string, relationType string) (int32, error) {
// CP 的“一人只能有一个”是产品硬约束,不能被后台或脏数据配置放宽。
if normalizeRelationType(relationType) == cpdomain.RelationTypeCP {
return 1, nil
}
var maxCount int32
err := tx.QueryRowContext(ctx, `
SELECT max_count_per_user
FROM user_cp_relation_configs
WHERE app_code = ? AND relation_type = ? AND status = 'active'
FOR UPDATE`, appCode, relationType).Scan(&maxCount)
if err == sql.ErrNoRows {
return 0, xerr.New(xerr.Conflict, "cp relation type is disabled")
}
if maxCount <= 0 {
maxCount = 1
}
return maxCount, err
}
func addRelationshipIntimacyTx(ctx context.Context, tx *sql.Tx, appCode string, relationship cpdomain.Relationship, delta int64, viewerUserID int64, nowMs int64) (cpdomain.Relationship, error) {
if delta < 0 {
delta = 0
}
nextValue := relationship.IntimacyValue + delta
// 等级直接由当前亲密值重算,避免漏处理一次大额礼物跨多级升级。
nextLevel, err := levelForIntimacyTx(ctx, tx, appCode, relationship.RelationType, nextValue)
if err != nil {
return cpdomain.Relationship{}, err
}
if _, err := tx.ExecContext(ctx, `
UPDATE user_cp_relationships
SET intimacy_value = ?, level_no = ?, updated_at_ms = ?
WHERE app_code = ? AND relationship_id = ?`,
nextValue, nextLevel, nowMs, appCode, relationship.RelationshipID,
); err != nil {
return cpdomain.Relationship{}, err
}
return queryRelationshipByIDTx(ctx, tx, appCode, viewerUserID, relationship.RelationshipID)
}
func levelForIntimacyTx(ctx context.Context, tx *sql.Tx, appCode string, relationType string, intimacy int64) (int32, error) {
rows, err := tx.QueryContext(ctx, `
SELECT level_no, intimacy_threshold
FROM user_cp_level_rules
WHERE app_code = ? AND relation_type = ? AND status = 'active'
ORDER BY level_no ASC`, appCode, relationType)
if err != nil {
return 1, err
}
defer rows.Close()
level := int32(1)
for rows.Next() {
var candidate int32
var threshold int64
if err := rows.Scan(&candidate, &threshold); err != nil {
return 1, err
}
if intimacy >= threshold {
level = candidate
}
}
return level, rows.Err()
}
func upsertApplicationFromGiftTx(ctx context.Context, tx *sql.Tx, appCode string, event cpdomain.GiftEvent, relationType string, nowMs int64) (cpdomain.Application, error) {
expireHours, err := applicationExpireHoursTx(ctx, tx, appCode, relationType)
if err != nil {
return cpdomain.Application{}, err
}
expiresAtMS := nowMs + int64(expireHours)*time.Hour.Milliseconds()
row := tx.QueryRowContext(ctx, applicationSelectSQL(`
WHERE a.app_code = ?
AND a.requester_user_id = ?
AND a.target_user_id = ?
AND a.relation_type = ?
AND a.status = ?
AND a.expires_at_ms > ?
ORDER BY a.created_at_ms DESC
LIMIT 1
FOR UPDATE`),
appCode, event.SenderUserID, event.TargetUserID, relationType, cpdomain.ApplicationStatusPending, nowMs,
)
application, err := scanApplication(row)
if err != nil && err != sql.ErrNoRows {
return cpdomain.Application{}, err
}
if err == nil {
// 同一对用户、同一类型已有 pending 申请时刷新礼物快照和过期时间,不生成多条待处理卡片。
if _, err := tx.ExecContext(ctx, `
UPDATE user_cp_applications
SET room_id = ?, room_region_id = ?, gift_id = ?, gift_name = ?, gift_icon_url = ?,
gift_animation_url = ?, gift_count = ?, gift_value = ?, billing_receipt_id = ?,
source_room_event_id = ?, source_command_id = ?, expires_at_ms = ?, updated_at_ms = ?
WHERE app_code = ? AND application_id = ?`,
event.RoomID, event.VisibleRegionID, event.GiftID, event.GiftName, event.GiftIconURL,
event.GiftAnimationURL, event.GiftCount, event.GiftValue, event.BillingReceiptID,
event.EventID, event.CommandID, expiresAtMS, nowMs, appCode, application.ApplicationID,
); err != nil {
return cpdomain.Application{}, err
}
application.RoomID = event.RoomID
application.RoomRegionID = event.VisibleRegionID
application.Gift = giftSnapshotFromEvent(event)
application.SourceEventID = event.EventID
application.SourceCommandID = event.CommandID
application.ExpiresAtMS = expiresAtMS
application.UpdatedAtMS = nowMs
return application, nil
}
applicationID := fmt.Sprintf("cp_app:%s", strings.TrimSpace(event.EventID))
// 新申请 ID 派生自 room outbox event_id结合消费位点可以保证 MQ 重投不会生成第二条申请。
if _, err := tx.ExecContext(ctx, `
INSERT INTO user_cp_applications (
app_code, application_id, requester_user_id, target_user_id, relation_type, status,
room_id, room_region_id, gift_id, gift_name, gift_icon_url, gift_animation_url,
gift_count, gift_value, billing_receipt_id, source_room_event_id, source_command_id,
created_at_ms, updated_at_ms, expires_at_ms, decided_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)`,
appCode, applicationID, event.SenderUserID, event.TargetUserID, relationType, cpdomain.ApplicationStatusPending,
event.RoomID, event.VisibleRegionID, event.GiftID, event.GiftName, event.GiftIconURL, event.GiftAnimationURL,
event.GiftCount, event.GiftValue, event.BillingReceiptID, event.EventID, event.CommandID,
nowMs, nowMs, expiresAtMS,
); err != nil {
return cpdomain.Application{}, err
}
row = tx.QueryRowContext(ctx, applicationSelectSQL(`
WHERE a.app_code = ? AND a.application_id = ?`), appCode, applicationID)
return scanApplication(row)
}
func applicationExpireHoursTx(ctx context.Context, tx *sql.Tx, appCode string, relationType string) (int32, error) {
var hours int32
err := tx.QueryRowContext(ctx, `
SELECT application_expire_hours
FROM user_cp_relation_configs
WHERE app_code = ? AND relation_type = ? AND status = 'active'`, appCode, relationType).Scan(&hours)
if err == sql.ErrNoRows {
return 0, xerr.New(xerr.Conflict, "cp relation type is disabled")
}
if hours <= 0 {
hours = 24
}
return hours, err
}
func lockConsumedEventTx(ctx context.Context, tx *sql.Tx, appCode string, eventID string) (bool, error) {
// 消费位点也加锁,防止同一 event_id 在并行 consumer 中同时进入创建/加亲密值分支。
var found string
err := tx.QueryRowContext(ctx, `
SELECT room_event_id
FROM user_cp_gift_event_consumption
WHERE app_code = ? AND room_event_id = ?
FOR UPDATE`, appCode, eventID).Scan(&found)
if err == sql.ErrNoRows {
return false, nil
}
return err == nil, err
}
func insertConsumptionTx(ctx context.Context, tx *sql.Tx, appCode string, eventID string, status string, applicationID string, relationshipID string, skipReason string, nowMs int64) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO user_cp_gift_event_consumption (
app_code, room_event_id, status, application_id, relationship_id,
skip_reason, consumed_at_ms, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
appCode, eventID, status, applicationID, relationshipID, skipReason, nowMs, nowMs, nowMs,
)
return err
}
func insertOutboxTx(ctx context.Context, tx *sql.Tx, appCode string, eventType string, aggregateType string, aggregateID int64, payload any, nowMs int64) error {
payloadBytes, err := json.Marshal(payload)
if err != nil {
return err
}
eventID := fmt.Sprintf("%s:%s:%d:%d", aggregateType, eventType, aggregateID, nowMs)
// outbox 与业务状态同事务提交RocketMQ 发布失败只影响异步 IM/统计,不回滚已经同意的关系。
_, err = tx.ExecContext(ctx, `
INSERT INTO user_outbox (
app_code, event_id, event_type, aggregate_type, aggregate_id,
status, worker_id, lock_until_ms, retry_count, next_retry_at_ms,
last_error, payload_json, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, 'pending', '', 0, 0, 0, '', CAST(? AS JSON), ?, ?)
ON DUPLICATE KEY UPDATE event_id = VALUES(event_id)`,
appCode, eventID, eventType, aggregateType, aggregateID, string(payloadBytes), nowMs, nowMs,
)
return err
}
func countRows(ctx context.Context, q interface {
QueryRowContext(context.Context, string, ...any) *sql.Row
}, query string, args ...any) (int64, error) {
var total int64
err := q.QueryRowContext(ctx, query, args...).Scan(&total)
return total, err
}
func normalizeRelationType(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case cpdomain.RelationTypeCP:
return cpdomain.RelationTypeCP
case cpdomain.RelationTypeBrother:
return cpdomain.RelationTypeBrother
case cpdomain.RelationTypeSister:
return cpdomain.RelationTypeSister
default:
return ""
}
}
func orderedPair(a int64, b int64) (int64, int64) {
if a < b {
return a, b
}
return b, a
}
func relationshipIDForPair(userAID int64, userBID int64) string {
// 关系 ID 固定由无序 pair 派生,配合 active 唯一约束保证 A/B 不会同时有多种 active 关系。
return fmt.Sprintf("cp_rel:%d:%d", userAID, userBID)
}
func giftSnapshotFromEvent(event cpdomain.GiftEvent) cpdomain.GiftSnapshot {
return cpdomain.GiftSnapshot{
GiftID: strings.TrimSpace(event.GiftID),
GiftName: strings.TrimSpace(event.GiftName),
GiftIconURL: strings.TrimSpace(event.GiftIconURL),
GiftAnimationURL: strings.TrimSpace(event.GiftAnimationURL),
GiftCount: event.GiftCount,
GiftValue: event.GiftValue,
BillingReceiptID: strings.TrimSpace(event.BillingReceiptID),
}
}
func clampPageSize(pageSize int32) int32 {
if pageSize <= 0 {
return 20
}
if pageSize > 100 {
return 100
}
return pageSize
}
func offset(page int32, pageSize int32) int32 {
if page <= 1 {
return 0
}
return (page - 1) * clampPageSize(pageSize)
}

View File

@ -0,0 +1,187 @@
package cp
import (
"context"
"database/sql"
"fmt"
"testing"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
cpdomain "hyapp/services/user-service/internal/domain/cp"
"hyapp/services/user-service/internal/testutil/mysqlschema"
)
func TestExpiredApplicationOperationsPersistExpiredStatus(t *testing.T) {
tests := []struct {
name string
applicationID string
run func(context.Context, *Repository, string, int64) error
}{
{
name: "accept expired",
applicationID: "cp_expired_accept",
run: func(ctx context.Context, repo *Repository, applicationID string, nowMS int64) error {
_, _, err := repo.AcceptApplication(ctx, 1002, applicationID, nowMS)
return err
},
},
{
name: "reject expired",
applicationID: "cp_expired_reject",
run: func(ctx context.Context, repo *Repository, applicationID string, nowMS int64) error {
_, err := repo.RejectApplication(ctx, 1002, applicationID, "too late", nowMS)
return err
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
schema := mysqlschema.New(t)
repo := New(schema.DB)
ctx := appcode.WithContext(context.Background(), appcode.Default)
nowMS := int64(1700000000000)
seedCPTestUser(t, schema.DB, 1001, "CP Alice", nowMS)
seedCPTestUser(t, schema.DB, 1002, "CP Bob", nowMS)
seedExpiredCPApplication(t, schema.DB, test.applicationID, 1001, 1002, nowMS)
err := test.run(ctx, repo, test.applicationID, nowMS)
if xerr.CodeOf(err) != xerr.Conflict {
t.Fatalf("expired operation must return conflict, got %v", err)
}
var status string
if err := schema.DB.QueryRowContext(ctx, `
SELECT status
FROM user_cp_applications
WHERE app_code = ? AND application_id = ?`,
appcode.Default, test.applicationID,
).Scan(&status); err != nil {
t.Fatalf("read application status failed: %v", err)
}
if status != cpdomain.ApplicationStatusExpired {
t.Fatalf("expired operation must persist status=%s, got %s", cpdomain.ApplicationStatusExpired, status)
}
})
}
}
func TestAcceptApplicationUsesSiblingCapacityConfig(t *testing.T) {
schema := mysqlschema.New(t)
repo := New(schema.DB)
ctx := appcode.WithContext(context.Background(), appcode.Default)
nowMS := int64(1700000000000)
for _, userID := range []int64{2001, 2002, 2003, 2004} {
seedCPTestUser(t, schema.DB, userID, fmt.Sprintf("Brother %d", userID), nowMS)
}
seedRelationConfig(t, schema.DB, cpdomain.RelationTypeBrother, 2, nowMS)
seedPendingCPApplication(t, schema.DB, "brother_1", cpdomain.RelationTypeBrother, 2001, 2002, nowMS)
seedPendingCPApplication(t, schema.DB, "brother_2", cpdomain.RelationTypeBrother, 2001, 2003, nowMS)
seedPendingCPApplication(t, schema.DB, "brother_3", cpdomain.RelationTypeBrother, 2001, 2004, nowMS)
if _, _, err := repo.AcceptApplication(ctx, 2002, "brother_1", nowMS); err != nil {
t.Fatalf("accept first brother relationship failed: %v", err)
}
if _, _, err := repo.AcceptApplication(ctx, 2003, "brother_2", nowMS+1); err != nil {
t.Fatalf("accept second brother relationship failed: %v", err)
}
if _, _, err := repo.AcceptApplication(ctx, 2004, "brother_3", nowMS+2); xerr.CodeOf(err) != xerr.Conflict {
t.Fatalf("third brother relationship must hit configured limit, got %v", err)
}
}
func TestAcceptApplicationKeepsCPHardLimit(t *testing.T) {
schema := mysqlschema.New(t)
repo := New(schema.DB)
ctx := appcode.WithContext(context.Background(), appcode.Default)
nowMS := int64(1700000000000)
for _, userID := range []int64{3001, 3002, 3003} {
seedCPTestUser(t, schema.DB, userID, fmt.Sprintf("CP %d", userID), nowMS)
}
seedRelationConfig(t, schema.DB, cpdomain.RelationTypeCP, 9, nowMS)
seedPendingCPApplication(t, schema.DB, "cp_1", cpdomain.RelationTypeCP, 3001, 3002, nowMS)
seedPendingCPApplication(t, schema.DB, "cp_2", cpdomain.RelationTypeCP, 3001, 3003, nowMS)
if _, _, err := repo.AcceptApplication(ctx, 3002, "cp_1", nowMS); err != nil {
t.Fatalf("accept first cp relationship failed: %v", err)
}
if _, _, err := repo.AcceptApplication(ctx, 3003, "cp_2", nowMS+1); xerr.CodeOf(err) != xerr.Conflict {
t.Fatalf("second cp relationship must ignore expanded config and stay limited, got %v", err)
}
}
func seedCPTestUser(t testing.TB, db *sql.DB, userID int64, username string, nowMS int64) {
t.Helper()
displayID := fmt.Sprintf("%d", userID)
// CP 申请查询会 join requester/target 用户快照;测试只准备这些不可空主数据字段。
if _, err := db.ExecContext(context.Background(), `
INSERT INTO users (
app_code, user_id, default_display_user_id, current_display_user_id, current_display_user_id_kind,
username, gender, country, region_id, avatar, profile_completed, profile_completed_at_ms,
onboarding_status, status, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, 'default', ?, 'unknown', 'US', 1, ?, 1, ?, 'completed', 'active', ?, ?)`,
appcode.Default, userID, displayID, displayID, username, "https://cdn.example.com/cp-test.png", nowMS, nowMS, nowMS,
); err != nil {
t.Fatalf("seed cp test user %d failed: %v", userID, err)
}
}
func seedRelationConfig(t testing.TB, db *sql.DB, relationType string, maxCount int32, nowMS int64) {
t.Helper()
// 容量测试直接写配置表AcceptApplication 中的默认配置 upsert 不会覆盖已有业务配置。
if _, err := db.ExecContext(context.Background(), `
INSERT INTO user_cp_relation_configs (
app_code, relation_type, max_count_per_user, application_expire_hours,
status, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, 24, 'active', ?, ?)
ON DUPLICATE KEY UPDATE
max_count_per_user = VALUES(max_count_per_user),
updated_at_ms = VALUES(updated_at_ms)`,
appcode.Default, relationType, maxCount, nowMS, nowMS,
); err != nil {
t.Fatalf("seed relation config %s failed: %v", relationType, err)
}
}
func seedPendingCPApplication(t testing.TB, db *sql.DB, applicationID string, relationType string, requesterUserID int64, targetUserID int64, nowMS int64) {
t.Helper()
if _, err := db.ExecContext(context.Background(), `
INSERT INTO user_cp_applications (
app_code, application_id, requester_user_id, target_user_id, relation_type, status,
room_id, room_region_id, gift_id, gift_name, gift_icon_url, gift_animation_url,
gift_count, gift_value, billing_receipt_id, source_room_event_id, source_command_id,
created_at_ms, updated_at_ms, expires_at_ms, decided_at_ms
) VALUES (?, ?, ?, ?, ?, ?, 'cp_test_room', 1, 'cp_test_gift', 'CP Test Gift', '', '',
1, 10, 'cp_test_receipt', ?, 'cp_test_command', ?, ?, ?, 0)`,
appcode.Default, applicationID, requesterUserID, targetUserID, relationType,
cpdomain.ApplicationStatusPending, "event_"+applicationID, nowMS, nowMS, nowMS+24*60*60*1000,
); err != nil {
t.Fatalf("seed pending cp application %s failed: %v", applicationID, err)
}
}
func seedExpiredCPApplication(t testing.TB, db *sql.DB, applicationID string, requesterUserID int64, targetUserID int64, nowMS int64) {
t.Helper()
// expires_at_ms 放在 nowMS 前一毫秒,确保仓储操作走“不能操作但要落 expired”的惰性过期分支。
if _, err := db.ExecContext(context.Background(), `
INSERT INTO user_cp_applications (
app_code, application_id, requester_user_id, target_user_id, relation_type, status,
room_id, room_region_id, gift_id, gift_name, gift_icon_url, gift_animation_url,
gift_count, gift_value, billing_receipt_id, source_room_event_id, source_command_id,
created_at_ms, updated_at_ms, expires_at_ms, decided_at_ms
) VALUES (?, ?, ?, ?, ?, ?, 'cp_test_room', 1, 'cp_test_gift', 'CP Test Gift', '', '',
1, 10, 'cp_test_receipt', ?, 'cp_test_command', ?, ?, ?, 0)`,
appcode.Default, applicationID, requesterUserID, targetUserID, cpdomain.RelationTypeCP,
cpdomain.ApplicationStatusPending, "event_"+applicationID, nowMS-1000, nowMS-1000, nowMS-1,
); err != nil {
t.Fatalf("seed expired cp application %s failed: %v", applicationID, err)
}
}

View File

@ -3,6 +3,7 @@ package mysql
import (
appstorage "hyapp/services/user-service/internal/storage/mysql/app"
authstorage "hyapp/services/user-service/internal/storage/mysql/auth"
cpstorage "hyapp/services/user-service/internal/storage/mysql/cp"
devicestorage "hyapp/services/user-service/internal/storage/mysql/device"
hoststorage "hyapp/services/user-service/internal/storage/mysql/host"
identitystorage "hyapp/services/user-service/internal/storage/mysql/identity"
@ -83,3 +84,11 @@ func (r *Repository) MicTimeRepository() *mictimestorage.Repository {
}
return mictimestorage.New(r.db)
}
// 基于 user-service 共享 MySQL 连接池返回 CP/兄弟/姐妹关系存储对象。
func (r *Repository) CPRepository() *cpstorage.Repository {
if r == nil {
return nil
}
return cpstorage.New(r.db)
}

View File

@ -285,6 +285,11 @@ func (r *Repository) RevokeSessionWithReason(ctx context.Context, sessionID stri
return r.Repository.AuthRepository().RevokeSessionWithReason(ctx, sessionID, revokedAtMs, reason, requestID, revokedBy)
}
// UpdateSessionHeartbeat 让测试 wrapper 继续满足认证接口。
func (r *Repository) UpdateSessionHeartbeat(ctx context.Context, sessionID string, userID int64, heartbeatAtMs int64, requestID string) (authdomain.Session, error) {
return r.Repository.AuthRepository().UpdateSessionHeartbeat(ctx, sessionID, userID, heartbeatAtMs, requestID)
}
// FindThirdPartyIdentity 让测试 wrapper 继续满足认证接口。
func (r *Repository) FindThirdPartyIdentity(ctx context.Context, provider string, providerSubject string) (authdomain.ThirdPartyIdentity, error) {
return r.Repository.AuthRepository().FindThirdPartyIdentity(ctx, provider, providerSubject)

View File

@ -0,0 +1,180 @@
package grpc
import (
"context"
userv1 "hyapp.local/api/proto/user/v1"
"hyapp/pkg/xerr"
cpdomain "hyapp/services/user-service/internal/domain/cp"
)
// ListCPApplications 返回当前用户收到或发出的 CP/兄弟/姐妹申请。
func (s *Server) ListCPApplications(ctx context.Context, req *userv1.ListCPApplicationsRequest) (*userv1.ListCPApplicationsResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
if s.cpSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp service is not configured"))
}
// gRPC 层只透传分页、方向和状态pending 过期固化由 service/repository 统一处理。
applications, total, serverTimeMS, err := s.cpSvc.ListApplications(ctx, req.GetUserId(), req.GetDirection(), req.GetStatus(), req.GetPage(), req.GetPageSize())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &userv1.ListCPApplicationsResponse{
Applications: make([]*userv1.CPApplication, 0, len(applications)),
Total: total,
ServerTimeMs: serverTimeMS,
}
for _, application := range applications {
resp.Applications = append(resp.Applications, toProtoCPApplication(application))
}
return resp, nil
}
// AcceptCPApplication 同意申请并返回建立后的关系。
func (s *Server) AcceptCPApplication(ctx context.Context, req *userv1.AcceptCPApplicationRequest) (*userv1.AcceptCPApplicationResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
if s.cpSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp service is not configured"))
}
application, relationship, err := s.cpSvc.AcceptApplication(ctx, req.GetUserId(), req.GetApplicationId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.AcceptCPApplicationResponse{
Application: toProtoCPApplication(application),
Relationship: toProtoCPRelationship(relationship),
}, nil
}
// RejectCPApplication 拒绝申请;拒绝原因只进入 outbox payload不改变关系主事实。
func (s *Server) RejectCPApplication(ctx context.Context, req *userv1.RejectCPApplicationRequest) (*userv1.RejectCPApplicationResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
if s.cpSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp service is not configured"))
}
application, err := s.cpSvc.RejectApplication(ctx, req.GetUserId(), req.GetApplicationId(), req.GetReason())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.RejectCPApplicationResponse{Application: toProtoCPApplication(application)}, nil
}
// ListCPRelationships 返回用户 active 关系;当前每个用户最多同时拥有一条关系。
func (s *Server) ListCPRelationships(ctx context.Context, req *userv1.ListCPRelationshipsRequest) (*userv1.ListCPRelationshipsResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
if s.cpSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp service is not configured"))
}
relationships, total, err := s.cpSvc.ListRelationships(ctx, req.GetUserId(), req.GetRelationType(), req.GetPage(), req.GetPageSize())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &userv1.ListCPRelationshipsResponse{
Relationships: make([]*userv1.CPRelationship, 0, len(relationships)),
Total: total,
}
for _, relationship := range relationships {
resp.Relationships = append(resp.Relationships, toProtoCPRelationship(relationship))
}
return resp, nil
}
// ConsumeRoomGiftCPEvent 由 user-service 内部 MQ worker 调用,把 RoomGiftSent 转为 CP 申请或亲密值增长。
func (s *Server) ConsumeRoomGiftCPEvent(ctx context.Context, req *userv1.ConsumeRoomGiftCPEventRequest) (*userv1.ConsumeRoomGiftCPEventResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
if s.cpSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp service is not configured"))
}
event := req.GetEvent()
result, err := s.cpSvc.ConsumeGiftEvent(ctx, cpdomain.GiftEvent{
EventID: event.GetEventId(),
RoomID: event.GetRoomId(),
RoomVersion: event.GetRoomVersion(),
OccurredAtMS: event.GetOccurredAtMs(),
SenderUserID: event.GetSenderUserId(),
TargetUserID: event.GetTargetUserId(),
GiftID: event.GetGiftId(),
GiftCount: event.GetGiftCount(),
GiftValue: event.GetGiftValue(),
BillingReceiptID: event.GetBillingReceiptId(),
VisibleRegionID: event.GetVisibleRegionId(),
CommandID: event.GetCommandId(),
GiftTypeCode: event.GetGiftTypeCode(),
CPRelationType: event.GetCpRelationType(),
GiftName: event.GetGiftName(),
GiftIconURL: event.GetGiftIconUrl(),
GiftAnimationURL: event.GetGiftAnimationUrl(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.ConsumeRoomGiftCPEventResponse{
Consumed: result.Consumed,
Application: toProtoCPApplication(result.Application),
Relationship: toProtoCPRelationship(result.Relationship),
}, nil
}
func toProtoCPApplication(application cpdomain.Application) *userv1.CPApplication {
if application.ApplicationID == "" {
return nil
}
return &userv1.CPApplication{
ApplicationId: application.ApplicationID,
RelationType: application.RelationType,
Status: application.Status,
Requester: toProtoCPUserProfile(application.Requester),
Target: toProtoCPUserProfile(application.Target),
RoomId: application.RoomID,
RoomRegionId: application.RoomRegionID,
Gift: toProtoCPGiftSnapshot(application.Gift),
CreatedAtMs: application.CreatedAtMS,
UpdatedAtMs: application.UpdatedAtMS,
ExpiresAtMs: application.ExpiresAtMS,
DecidedAtMs: application.DecidedAtMS,
}
}
func toProtoCPRelationship(relationship cpdomain.Relationship) *userv1.CPRelationship {
if relationship.RelationshipID == "" {
return nil
}
return &userv1.CPRelationship{
RelationshipId: relationship.RelationshipID,
RelationType: relationship.RelationType,
Status: relationship.Status,
Me: toProtoCPUserProfile(relationship.Me),
Partner: toProtoCPUserProfile(relationship.Partner),
IntimacyValue: relationship.IntimacyValue,
Level: relationship.Level,
FormedAtMs: relationship.FormedAtMS,
UpdatedAtMs: relationship.UpdatedAtMS,
}
}
func toProtoCPUserProfile(profile cpdomain.UserProfile) *userv1.CPUserProfile {
if profile.UserID <= 0 {
return nil
}
return &userv1.CPUserProfile{
UserId: profile.UserID,
DisplayUserId: profile.DisplayUserID,
Username: profile.Username,
Avatar: profile.Avatar,
}
}
func toProtoCPGiftSnapshot(gift cpdomain.GiftSnapshot) *userv1.CPGiftSnapshot {
if gift.GiftID == "" && gift.GiftName == "" {
return nil
}
return &userv1.CPGiftSnapshot{
GiftId: gift.GiftID,
GiftName: gift.GiftName,
GiftIconUrl: gift.GiftIconURL,
GiftAnimationUrl: gift.GiftAnimationURL,
GiftCount: gift.GiftCount,
GiftValue: gift.GiftValue,
BillingReceiptId: gift.BillingReceiptID,
}
}

View File

@ -11,6 +11,7 @@ import (
authdomain "hyapp/services/user-service/internal/domain/auth"
userdomain "hyapp/services/user-service/internal/domain/user"
authservice "hyapp/services/user-service/internal/service/auth"
cpservice "hyapp/services/user-service/internal/service/cp"
hostservice "hyapp/services/user-service/internal/service/host"
mictimeservice "hyapp/services/user-service/internal/service/mictime"
userservice "hyapp/services/user-service/internal/service/user"
@ -42,6 +43,10 @@ type Server struct {
userv1.UnimplementedUserHostAdminServiceServer
// UnimplementedUserCronServiceServer 提供 cron-service 批处理 RPC 的默认返回。
userv1.UnimplementedUserCronServiceServer
// UnimplementedUserCPServiceServer 提供 CP 关系 App RPC 的默认返回。
userv1.UnimplementedUserCPServiceServer
// UnimplementedUserCPInternalServiceServer 提供 CP 内部消费 RPC 的默认返回。
userv1.UnimplementedUserCPInternalServiceServer
// authSvc 承载登录、设置密码、refresh 和 logout。
authSvc *authservice.Service
@ -51,6 +56,8 @@ type Server struct {
hostSvc *hostservice.Service
// micTimeSvc 承载用户维度麦位在线时长聚合查询。
micTimeSvc *mictimeservice.Service
// cpSvc 承载 CP/兄弟/姐妹申请、关系和房间送礼消费用例。
cpSvc *cpservice.Service
}
// NewServer 创建 user-service gRPC server。
@ -72,6 +79,11 @@ func (s *Server) SetMicTimeService(micTimeSvc *mictimeservice.Service) {
s.micTimeSvc = micTimeSvc
}
// SetCPService 挂载 CP 关系 service保持 NewServer 老测试构造方式兼容。
func (s *Server) SetCPService(cpSvc *cpservice.Service) {
s.cpSvc = cpSvc
}
// ResolveApp 把客户端包名映射成内部 app_codegateway 后续会把 app_code 写入 JWT 和 RequestMeta。
func (s *Server) ResolveApp(ctx context.Context, req *userv1.ResolveAppRequest) (*userv1.ResolveAppResponse, error) {
app, err := s.userSvc.ResolveApp(ctx, req.GetAppCode(), req.GetPackageName(), req.GetPlatform())
@ -198,6 +210,21 @@ func (s *Server) RecordLoginBlocked(ctx context.Context, req *userv1.RecordLogin
return &userv1.RecordLoginBlockedResponse{Recorded: recorded}, nil
}
// AppHeartbeat 刷新当前 App 登录会话最近在线时间,不创建房间或麦位 presence。
func (s *Server) AppHeartbeat(ctx context.Context, req *userv1.AppHeartbeatRequest) (*userv1.AppHeartbeatResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
session, err := s.authSvc.AppHeartbeat(ctx, req.GetUserId(), req.GetSessionId(), authMeta(req.GetMeta()))
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.AppHeartbeatResponse{
Accepted: true,
HeartbeatAtMs: session.LastHeartbeatAtMs,
ServerTimeMs: session.LastHeartbeatAtMs,
}, nil
}
// GetUser 返回用户主状态。
func (s *Server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())

View File

@ -161,8 +161,14 @@ type Receipt struct {
GiftPointAdded int64
HeatValue int64
GiftTypeCode string
PriceVersion string
BalanceAfter int64
// CPRelationType 只在 CP 礼物类型上有值room-service 会把它写入 RoomGiftSent 供 user-service 建关系申请。
CPRelationType string
// 礼物展示字段是扣费时的资源快照,避免异步消费者再查礼物配置导致历史 IM 展示被后续配置修改影响。
GiftName string
GiftIconURL string
GiftAnimationURL string
PriceVersion string
BalanceAfter int64
// HostPeriodDiamondAdded 是本次送礼写入主播工资周期账户的钻石数;非主播恒为 0。
HostPeriodDiamondAdded int64
// HostPeriodCycleKey 是工资周期键,当前按 UTC 月生成,后续结算按此键定位周期账户。

View File

@ -151,6 +151,8 @@ type GiftConfig struct {
GiftPointAmount int64
HeatValue int64
GiftTypeCode string
// CPRelationType 只在 gift_type_code=cp 的礼物上使用,值为 cp、brother 或 sister。
CPRelationType string
ChargeAssetType string
EffectiveFromMS int64
EffectiveToMS int64
@ -347,6 +349,8 @@ type GiftConfigCommand struct {
HeatValue int64
EffectiveAtMS int64
GiftTypeCode string
// CPRelationType 会写入 presentation_json避免新增 gift_configs 表字段。
CPRelationType string
ChargeAssetType string
EffectiveFromMS int64
EffectiveToMS int64

View File

@ -16,6 +16,7 @@ import (
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
"hyapp/services/wallet-service/internal/domain/ledger"
resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
mysqlDriver "github.com/go-sql-driver/mysql"
)
@ -260,9 +261,12 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
AppCode: command.AppCode,
GiftID: command.GiftID,
GiftName: giftConfig.Name,
GiftIconURL: giftDisplayIconURL(giftConfig.Resource),
GiftAnimationURL: strings.TrimSpace(giftConfig.Resource.AnimationURL),
ResourceID: giftConfig.ResourceID,
ResourceSnapshot: mustJSON(giftConfig.Resource),
GiftTypeCode: giftConfig.GiftTypeCode,
CPRelationType: cpRelationTypeFromPresentationJSON(giftConfig.PresentationJSON),
PresentationJSON: giftConfig.PresentationJSON,
SortOrder: giftConfig.SortOrder,
GiftCount: command.GiftCount,
@ -477,9 +481,12 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb
AppCode: command.AppCode,
GiftID: command.GiftID,
GiftName: giftConfig.Name,
GiftIconURL: giftDisplayIconURL(giftConfig.Resource),
GiftAnimationURL: strings.TrimSpace(giftConfig.Resource.AnimationURL),
ResourceID: giftConfig.ResourceID,
ResourceSnapshot: mustJSON(giftConfig.Resource),
GiftTypeCode: giftConfig.GiftTypeCode,
CPRelationType: cpRelationTypeFromPresentationJSON(giftConfig.PresentationJSON),
PresentationJSON: giftConfig.PresentationJSON,
SortOrder: giftConfig.SortOrder,
GiftCount: command.GiftCount,
@ -2567,9 +2574,12 @@ type giftMetadata struct {
AppCode string `json:"app_code"`
GiftID string `json:"gift_id"`
GiftName string `json:"gift_name"`
GiftIconURL string `json:"gift_icon_url"`
GiftAnimationURL string `json:"gift_animation_url"`
ResourceID int64 `json:"resource_id"`
ResourceSnapshot string `json:"resource_snapshot_json"`
GiftTypeCode string `json:"gift_type_code"`
CPRelationType string `json:"cp_relation_type"`
PresentationJSON string `json:"presentation_json"`
SortOrder int32 `json:"sort_order"`
GiftCount int32 `json:"gift_count"`
@ -2763,6 +2773,10 @@ func receiptFromGiftMetadata(transactionID string, metadata giftMetadata) ledger
GiftPointAdded: 0,
HeatValue: metadata.HeatValue,
GiftTypeCode: metadata.GiftTypeCode,
CPRelationType: metadata.CPRelationType,
GiftName: metadata.GiftName,
GiftIconURL: metadata.GiftIconURL,
GiftAnimationURL: metadata.GiftAnimationURL,
PriceVersion: metadata.PriceVersion,
BalanceAfter: metadata.BalanceAfter,
HostPeriodDiamondAdded: metadata.HostPeriodDiamondAdded,
@ -2770,6 +2784,14 @@ func receiptFromGiftMetadata(transactionID string, metadata giftMetadata) ledger
}
}
func giftDisplayIconURL(resource resourcedomain.Resource) string {
// preview_url 是面板和 IM 的首选静态图;旧资源没有 preview 时回退 asset_url避免关系广播缺图。
if value := strings.TrimSpace(resource.PreviewURL); value != "" {
return value
}
return strings.TrimSpace(resource.AssetURL)
}
func (r *Repository) receiptForTaskRewardTransaction(ctx context.Context, tx *sql.Tx, transactionID string) (ledger.TaskRewardReceipt, error) {
var metadataJSON string
if err := tx.QueryRowContext(ctx,
@ -3393,6 +3415,18 @@ func aggregateGiftReceipts(targets []ledger.BatchGiftTargetReceipt) (ledger.Rece
if aggregate.GiftTypeCode == "" {
aggregate.GiftTypeCode = receipt.GiftTypeCode
}
if aggregate.CPRelationType == "" {
aggregate.CPRelationType = receipt.CPRelationType
}
if aggregate.GiftName == "" {
aggregate.GiftName = receipt.GiftName
}
if aggregate.GiftIconURL == "" {
aggregate.GiftIconURL = receipt.GiftIconURL
}
if aggregate.GiftAnimationURL == "" {
aggregate.GiftAnimationURL = receipt.GiftAnimationURL
}
if aggregate.PriceVersion == "" {
aggregate.PriceVersion = receipt.PriceVersion
}

View File

@ -1517,7 +1517,8 @@ func (r *Repository) upsertGiftConfig(ctx context.Context, command resourcedomai
return resourcedomain.GiftConfig{}, xerr.New(xerr.InvalidArgument, "gift type is not configured")
}
nowMs := time.Now().UnixMilli()
presentation := normalizeJSONObject(command.PresentationJSON)
// CP 关系类型存放在 presentation_json 内,保存前统一合并,避免 gift_configs 为单个业务展示字段扩表。
presentation := giftPresentationWithCPRelationType(command.PresentationJSON, command.CPRelationType)
effectTypesJSON, err := json.Marshal(command.EffectTypes)
if err != nil {
return resourcedomain.GiftConfig{}, err
@ -2602,6 +2603,8 @@ func scanGiftConfig(scanner scanTarget) (resourcedomain.GiftConfig, error) {
resource.UsageScopes = parseStringArray(scopesJSON)
gift.Resource = resource
gift.EffectTypes = parseStringArray(effectTypesJSON)
// CPRelationType 是展示 JSON 的结构化投影gift panel 和送礼回执不再让调用方自己解析 JSON。
gift.CPRelationType = cpRelationTypeFromPresentationJSON(gift.PresentationJSON)
return gift, nil
}
@ -3291,6 +3294,7 @@ func normalizeGiftConfigCommand(command resourcedomain.GiftConfigCommand) resour
command.Status = resourcedomain.NormalizeStatus(command.Status)
command.Name = strings.TrimSpace(command.Name)
command.PresentationJSON = normalizeJSONObject(command.PresentationJSON)
command.CPRelationType = normalizeCPRelationType(command.CPRelationType)
command.PriceVersion = strings.TrimSpace(command.PriceVersion)
command.RegionIDs = normalizeRegionIDs(command.RegionIDs)
command.GiftTypeCode = resourcedomain.NormalizeGiftTypeCode(command.GiftTypeCode)
@ -3334,6 +3338,9 @@ func validateGiftConfigCommand(command resourcedomain.GiftConfigCommand) error {
if !resourcedomain.ValidGiftTypeCode(command.GiftTypeCode) {
return xerr.New(xerr.InvalidArgument, "gift type is invalid")
}
if command.CPRelationType != "" && command.GiftTypeCode != resourcedomain.GiftTypeCP {
return xerr.New(xerr.InvalidArgument, "cp relation type requires cp gift type")
}
if !ledger.ValidGiftChargeAssetType(command.ChargeAssetType) {
return xerr.New(xerr.InvalidArgument, "gift charge asset type is invalid")
}
@ -3548,6 +3555,49 @@ func normalizeJSONObject(value string) string {
return value
}
func cpRelationTypeFromPresentationJSON(value string) string {
var payload map[string]any
if err := json.Unmarshal([]byte(normalizeJSONObject(value)), &payload); err != nil {
return ""
}
if raw, ok := payload["cp_relation_type"].(string); ok {
return normalizeCPRelationType(raw)
}
return ""
}
func giftPresentationWithCPRelationType(value string, relationType string) string {
relationType = normalizeCPRelationType(relationType)
var payload map[string]any
if err := json.Unmarshal([]byte(normalizeJSONObject(value)), &payload); err != nil || payload == nil {
payload = map[string]any{}
}
// 空值表示清除 CP 关系类型;非 CP 礼物保存时不能残留旧的 cp_relation_type。
if relationType == "" {
delete(payload, "cp_relation_type")
} else {
payload["cp_relation_type"] = relationType
}
encoded, err := json.Marshal(payload)
if err != nil {
return "{}"
}
return string(encoded)
}
func normalizeCPRelationType(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "cp":
return "cp"
case "brother":
return "brother"
case "sister":
return "sister"
default:
return ""
}
}
func resourceOutboxEvent(eventType string, commandID string, userID int64, resourceID int64, payload any, nowMs int64) walletOutboxEvent {
eventKey := fmt.Sprintf("%s|%s|%d|%d", eventType, commandID, userID, resourceID)
return walletOutboxEvent{

View File

@ -483,6 +483,7 @@ func giftConfigCommandFromCreate(req *walletv1.CreateGiftConfigRequest) resource
HeatValue: req.GetHeatValue(),
EffectiveAtMS: req.GetEffectiveAtMs(),
GiftTypeCode: req.GetGiftTypeCode(),
CPRelationType: req.GetCpRelationType(),
ChargeAssetType: req.GetChargeAssetType(),
EffectiveFromMS: req.GetEffectiveFromMs(),
EffectiveToMS: req.GetEffectiveToMs(),
@ -507,6 +508,7 @@ func giftConfigCommandFromUpdate(req *walletv1.UpdateGiftConfigRequest) resource
HeatValue: req.GetHeatValue(),
EffectiveAtMS: req.GetEffectiveAtMs(),
GiftTypeCode: req.GetGiftTypeCode(),
CPRelationType: req.GetCpRelationType(),
ChargeAssetType: req.GetChargeAssetType(),
EffectiveFromMS: req.GetEffectiveFromMs(),
EffectiveToMS: req.GetEffectiveToMs(),
@ -673,6 +675,7 @@ func giftConfigToProto(gift resourcedomain.GiftConfig) *walletv1.GiftConfig {
GiftPointAmount: 0,
HeatValue: gift.HeatValue,
GiftTypeCode: gift.GiftTypeCode,
CpRelationType: gift.CPRelationType,
EffectiveFromMs: gift.EffectiveFromMS,
EffectiveToMs: gift.EffectiveToMS,
EffectTypes: gift.EffectTypes,

View File

@ -102,6 +102,10 @@ func debitGiftResponseFromReceipt(receipt ledger.Receipt) *walletv1.DebitGiftRes
GiftPointAdded: receipt.GiftPointAdded,
HeatValue: receipt.HeatValue,
GiftTypeCode: receipt.GiftTypeCode,
CpRelationType: receipt.CPRelationType,
GiftName: receipt.GiftName,
GiftIconUrl: receipt.GiftIconURL,
GiftAnimationUrl: receipt.GiftAnimationURL,
PriceVersion: receipt.PriceVersion,
HostPeriodDiamondAdded: receipt.HostPeriodDiamondAdded,
HostPeriodCycleKey: receipt.HostPeriodCycleKey,

892
tools/cp_flow_smoke/main.go Normal file
View File

@ -0,0 +1,892 @@
// Package main runs a local end-to-end CP flow against real Docker services.
package main
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/golang-jwt/jwt/v5"
)
const (
appCode = "lalu"
jwtSecret = "dev-shared-secret"
userA = int64(910001)
userB = int64(910002)
userC = int64(910003)
giftCP = "local_cp_gift_cp"
giftBrother = "local_cp_gift_brother"
giftSister = "local_cp_gift_sister"
)
type smoke struct {
db *sql.DB
http *http.Client
gateway string
startMS int64
roomID string
region int64
}
type envelope struct {
Code string `json:"code"`
Message string `json:"message"`
RequestID string `json:"request_id"`
Data json.RawMessage `json:"data"`
}
type cpApplication struct {
ApplicationID string `json:"application_id"`
RelationType string `json:"relation_type"`
Status string `json:"status"`
RoomID string `json:"room_id"`
ExpiresAtMS int64 `json:"expires_at_ms"`
}
type cpRelationship struct {
RelationshipID string `json:"relationship_id"`
RelationType string `json:"relation_type"`
Status string `json:"status"`
IntimacyValue int64 `json:"intimacy_value"`
Level int32 `json:"level"`
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), envDuration("CP_SMOKE_TIMEOUT", 4*time.Minute))
defer cancel()
db, err := sql.Open("mysql", env("CP_SMOKE_MYSQL_DSN", "hyapp:hyapp@tcp(127.0.0.1:23306)/?parseTime=true&charset=utf8mb4&loc=UTC&multiStatements=true"))
if err != nil {
fatal(err)
}
defer db.Close()
if err := db.PingContext(ctx); err != nil {
fatal(fmt.Errorf("mysql ping failed: %w", err))
}
s := &smoke{
db: db,
http: &http.Client{Timeout: 8 * time.Second},
gateway: strings.TrimRight(env("CP_SMOKE_GATEWAY", "http://127.0.0.1:13000"), "/"),
startMS: time.Now().UTC().UnixMilli(),
}
steps := []struct {
name string
fn func(context.Context) error
}{
{"seed users, wallet, gifts", s.seed},
{"import A/B/C Tencent IM accounts", s.importIMAccounts},
{"create room and join A/B/C", s.createRoomAndJoin},
{"T01 create CP application from gift", s.testCreateCPApplication},
{"T02 accept application and deliver C2C/room/region IM", s.testAcceptApplication},
{"T03 existing relationship gift increments intimacy", s.testIntimacyIncrement},
{"T04 same pair can accept only one relation type", s.testPairSingleRelation},
{"T05 one user can have only one active relation", s.testUserSingleRelation},
{"T06 expired application cannot be accepted", s.testExpiredApplication},
{"T07 rejected application delivers C2C and room IM", s.testRejectApplication},
}
for _, step := range steps {
fmt.Printf("==> %s\n", step.name)
if err := step.fn(ctx); err != nil {
fatal(fmt.Errorf("%s: %w", step.name, err))
}
fmt.Printf("ok %s\n", step.name)
}
fmt.Println("CP smoke flow passed")
}
func (s *smoke) seed(ctx context.Context) error {
regionID, err := s.lookupUSRegion(ctx)
if err != nil {
return err
}
s.region = regionID
if err := s.cleanupCPFacts(ctx); err != nil {
return err
}
if err := s.cleanupRoomFacts(ctx); err != nil {
return err
}
if err := s.seedUsers(ctx, regionID); err != nil {
return err
}
if err := s.seedWallet(ctx); err != nil {
return err
}
return s.seedGifts(ctx)
}
func (s *smoke) lookupUSRegion(ctx context.Context) (int64, error) {
var regionID int64
err := s.db.QueryRowContext(ctx, `
SELECT r.region_id
FROM hyapp_user.regions r
JOIN hyapp_user.region_countries rc ON rc.app_code = r.app_code AND rc.region_id = r.region_id
WHERE r.app_code = ? AND rc.country_code = 'US' AND r.status = 'active' AND rc.status = 'active'
ORDER BY r.region_id
LIMIT 1`, appCode).Scan(&regionID)
if err != nil {
return 0, fmt.Errorf("lookup US region: %w", err)
}
return regionID, nil
}
func (s *smoke) cleanupCPFacts(ctx context.Context) error {
ids := []int64{userA, userB, userC}
relationshipIDAB := relationshipID(userA, userB)
relationshipIDAC := relationshipID(userA, userC)
statements := []string{
`DELETE FROM hyapp_user.user_cp_level_reward_grants WHERE app_code = ? AND relationship_id IN (?, ?)`,
`DELETE FROM hyapp_user.user_cp_relationships WHERE app_code = ? AND (user_a_id IN (?, ?, ?) OR user_b_id IN (?, ?, ?))`,
`DELETE FROM hyapp_user.user_cp_applications WHERE app_code = ? AND (requester_user_id IN (?, ?, ?) OR target_user_id IN (?, ?, ?))`,
`DELETE FROM hyapp_user.user_outbox WHERE app_code = ? AND event_type LIKE 'UserCP%' AND aggregate_id IN (?, ?, ?)`,
`DELETE FROM hyapp_notice.notice_delivery_events
WHERE source_name = 'user_outbox' AND app_code = ? AND notice_type LIKE 'cp_%'
AND (JSON_SEARCH(payload_json, 'one', ?) IS NOT NULL
OR JSON_SEARCH(payload_json, 'one', ?) IS NOT NULL
OR JSON_SEARCH(payload_json, 'one', ?) IS NOT NULL)`,
}
if _, err := s.db.ExecContext(ctx, statements[0], appCode, relationshipIDAB, relationshipIDAC); err != nil && !isMissingTable(err) {
return err
}
if _, err := s.db.ExecContext(ctx, statements[1], appCode, ids[0], ids[1], ids[2], ids[0], ids[1], ids[2]); err != nil {
return err
}
if _, err := s.db.ExecContext(ctx, statements[2], appCode, ids[0], ids[1], ids[2], ids[0], ids[1], ids[2]); err != nil {
return err
}
if _, err := s.db.ExecContext(ctx, statements[3], appCode, ids[0], ids[1], ids[2]); err != nil {
return err
}
_, err := s.db.ExecContext(ctx, statements[4], appCode, fmt.Sprint(userA), fmt.Sprint(userB), fmt.Sprint(userC))
return err
}
func (s *smoke) cleanupRoomFacts(ctx context.Context) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
// smoke 使用固定用户反复造房room-service 对 owner_user_id 有唯一约束,所以每次运行前必须清掉这些测试用户拥有的旧房间事实。
// 所有删除都先通过 rooms.owner_user_id 限定范围,最后才删 rooms 本表,避免误清理其他本地调试房间。
roomSubquery := `SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?, ?)`
deleteByRoomStatements := []string{
`DELETE FROM hyapp_room.room_background_images WHERE app_code = ? AND room_id IN (` + roomSubquery + `)`,
`DELETE FROM hyapp_room.room_snapshots WHERE app_code = ? AND room_id IN (` + roomSubquery + `)`,
`DELETE FROM hyapp_room.room_command_log WHERE app_code = ? AND room_id IN (` + roomSubquery + `)`,
`DELETE FROM hyapp_room.room_outbox WHERE app_code = ? AND room_id IN (` + roomSubquery + `)`,
`DELETE FROM hyapp_room.room_user_gift_stats WHERE app_code = ? AND room_id IN (` + roomSubquery + `)`,
`DELETE FROM hyapp_room.room_user_feed_entries WHERE app_code = ? AND room_id IN (` + roomSubquery + `)`,
`DELETE FROM hyapp_room.room_follows WHERE app_code = ? AND room_id IN (` + roomSubquery + `)`,
`DELETE FROM hyapp_room.room_region_pins WHERE app_code = ? AND room_id IN (` + roomSubquery + `)`,
`DELETE FROM hyapp_room.room_list_entries WHERE app_code = ? AND room_id IN (` + roomSubquery + `)`,
}
for _, statement := range deleteByRoomStatements {
if _, err := tx.ExecContext(ctx, statement, appCode, appCode, userA, userB, userC); err != nil {
return err
}
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM hyapp_room.room_user_presence
WHERE app_code = ?
AND (user_id IN (?, ?, ?) OR room_id IN (`+roomSubquery+`))`,
appCode, userA, userB, userC, appCode, userA, userB, userC,
); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM hyapp_room.rooms
WHERE app_code = ? AND owner_user_id IN (?, ?, ?)`,
appCode, userA, userB, userC,
); err != nil {
return err
}
return tx.Commit()
}
func (s *smoke) seedUsers(ctx context.Context, regionID int64) error {
if _, err := s.db.ExecContext(ctx, `
DELETE FROM hyapp_user.user_display_user_ids
WHERE app_code = ? AND (user_id IN (?, ?, ?) OR display_user_id IN ('910001', '910002', '910003'))`,
appCode, userA, userB, userC,
); err != nil {
return err
}
users := []struct {
id int64
name string
avatar string
}{
{userA, "CP Smoke Alice", "https://cdn.example.com/cp-smoke-a.png"},
{userB, "CP Smoke Bob", "https://cdn.example.com/cp-smoke-b.png"},
{userC, "CP Smoke Cara", "https://cdn.example.com/cp-smoke-c.png"},
}
nowMS := time.Now().UTC().UnixMilli()
for _, user := range users {
displayID := strconv.FormatInt(user.id, 10)
if _, err := s.db.ExecContext(ctx, `
INSERT INTO hyapp_user.users (
app_code, user_id, default_display_user_id, current_display_user_id, current_display_user_id_kind,
username, gender, country, region_id, avatar, profile_completed, profile_completed_at_ms,
onboarding_status, status, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, 'default', ?, 'unknown', 'US', ?, ?, 1, ?, 'completed', 'active', ?, ?)
ON DUPLICATE KEY UPDATE
current_display_user_id = VALUES(current_display_user_id),
current_display_user_id_kind = VALUES(current_display_user_id_kind),
username = VALUES(username),
country = VALUES(country),
region_id = VALUES(region_id),
avatar = VALUES(avatar),
profile_completed = VALUES(profile_completed),
profile_completed_at_ms = VALUES(profile_completed_at_ms),
onboarding_status = VALUES(onboarding_status),
status = VALUES(status),
updated_at_ms = VALUES(updated_at_ms)`,
appCode, user.id, displayID, displayID, user.name, regionID, user.avatar, nowMS, nowMS, nowMS,
); err != nil {
return err
}
if _, err := s.db.ExecContext(ctx, `
INSERT INTO hyapp_user.user_display_user_ids (
app_code, display_user_id, user_id, display_user_id_kind, status,
assigned_at_ms, activated_at_ms, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, 'default', 'active', ?, ?, ?, ?)`,
appCode, displayID, user.id, nowMS, nowMS, nowMS, nowMS,
); err != nil {
return err
}
}
return nil
}
func (s *smoke) seedWallet(ctx context.Context) error {
nowMS := time.Now().UTC().UnixMilli()
for _, userID := range []int64{userA, userC} {
if _, err := s.db.ExecContext(ctx, `
INSERT INTO hyapp_wallet.wallet_accounts (
app_code, user_id, asset_type, available_amount, frozen_amount, version, created_at_ms, updated_at_ms
) VALUES (?, ?, 'COIN', 1000000, 0, 1, ?, ?)
ON DUPLICATE KEY UPDATE available_amount = 1000000, frozen_amount = 0, version = version + 1, updated_at_ms = VALUES(updated_at_ms)`,
appCode, userID, nowMS, nowMS,
); err != nil {
return err
}
}
return nil
}
func (s *smoke) seedGifts(ctx context.Context) error {
gifts := []struct {
id string
resourceCode string
name string
relationType string
icon string
}{
{giftCP, "local_cp_gift_cp_resource", "Local CP Gift", "cp", "https://cdn.example.com/local-cp.png"},
{giftBrother, "local_cp_gift_brother_resource", "Local Brother Gift", "brother", "https://cdn.example.com/local-brother.png"},
{giftSister, "local_cp_gift_sister_resource", "Local Sister Gift", "sister", "https://cdn.example.com/local-sister.png"},
}
nowMS := time.Now().UTC().UnixMilli()
for index, gift := range gifts {
resourceID, err := s.upsertGiftResource(ctx, gift.resourceCode, gift.name, gift.icon, int32(index+1)*10, nowMS)
if err != nil {
return err
}
presentation := fmt.Sprintf(`{"cp_relation_type":"%s","cp_relation_label":"%s"}`, gift.relationType, gift.name)
if _, err := s.db.ExecContext(ctx, `
INSERT INTO hyapp_wallet.gift_configs (
app_code, gift_id, resource_id, status, name, sort_order, presentation_json,
gift_type_code, effective_from_ms, effective_to_ms, effect_types_json,
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, 'active', ?, ?, CAST(? AS JSON), 'cp', 0, 0, JSON_ARRAY(), 0, 0, ?, ?)
ON DUPLICATE KEY UPDATE
resource_id = VALUES(resource_id),
status = 'active',
name = VALUES(name),
sort_order = VALUES(sort_order),
presentation_json = VALUES(presentation_json),
gift_type_code = 'cp',
effective_from_ms = 0,
effective_to_ms = 0,
effect_types_json = JSON_ARRAY(),
updated_at_ms = VALUES(updated_at_ms)`,
appCode, gift.id, resourceID, gift.name, int32(index+1)*10, presentation, nowMS, nowMS,
); err != nil {
return err
}
if _, err := s.db.ExecContext(ctx, `
INSERT INTO hyapp_wallet.wallet_gift_prices (
app_code, gift_id, price_version, status, charge_asset_type, coin_price, gift_point_amount,
heat_value, effective_at_ms, created_at_ms, updated_at_ms
) VALUES (?, ?, 'smoke-v1', 'active', 'COIN', 10, 0, 10, 0, ?, ?)
ON DUPLICATE KEY UPDATE
status = 'active',
charge_asset_type = 'COIN',
coin_price = 10,
gift_point_amount = 0,
heat_value = 10,
effective_at_ms = 0,
updated_at_ms = VALUES(updated_at_ms)`,
appCode, gift.id, nowMS, nowMS,
); err != nil {
return err
}
if _, err := s.db.ExecContext(ctx, `
INSERT INTO hyapp_wallet.gift_config_regions (app_code, gift_id, region_id, created_at_ms, updated_at_ms)
VALUES (?, ?, 0, ?, ?)
ON DUPLICATE KEY UPDATE updated_at_ms = VALUES(updated_at_ms)`,
appCode, gift.id, nowMS, nowMS,
); err != nil {
return err
}
}
return nil
}
func (s *smoke) upsertGiftResource(ctx context.Context, code string, name string, icon string, sortOrder int32, nowMS int64) (int64, error) {
if _, err := s.db.ExecContext(ctx, `
INSERT INTO hyapp_wallet.resources (
app_code, resource_code, resource_type, name, status, grantable, manager_grant_enabled,
grant_strategy, wallet_asset_type, wallet_asset_amount, price_type, coin_price, gift_point_amount,
usage_scope_json, asset_url, preview_url, animation_url, metadata_json, sort_order,
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
) VALUES (?, ?, 'gift', ?, 'active', TRUE, TRUE, 'permanent', '', 0, 'coin', 10, 0,
JSON_ARRAY('gift_panel'), ?, ?, '', JSON_OBJECT(), ?, 0, 0, ?, ?)
ON DUPLICATE KEY UPDATE
resource_type = 'gift',
name = VALUES(name),
status = 'active',
asset_url = VALUES(asset_url),
preview_url = VALUES(preview_url),
sort_order = VALUES(sort_order),
updated_at_ms = VALUES(updated_at_ms)`,
appCode, code, name, icon, icon, sortOrder, nowMS, nowMS,
); err != nil {
return 0, err
}
var resourceID int64
err := s.db.QueryRowContext(ctx, `SELECT resource_id FROM hyapp_wallet.resources WHERE app_code = ? AND resource_code = ?`, appCode, code).Scan(&resourceID)
return resourceID, err
}
func (s *smoke) importIMAccounts(ctx context.Context) error {
for _, userID := range []int64{userA, userB, userC} {
if _, err := s.request(ctx, http.MethodGet, "/api/v1/im/usersig", userID, nil); err != nil {
return fmt.Errorf("import im account %d: %w", userID, err)
}
}
return nil
}
func (s *smoke) createRoomAndJoin(ctx context.Context) error {
// CreateRoom 走真实 gateway->room-service 校验seat_count 必须使用后台允许的麦位数,
// 这里固定 10 麦避免测试数据绕过当前房间配置边界。
resp, err := s.request(ctx, http.MethodPost, "/api/v1/rooms/create", userA, map[string]any{
"command_id": commandID("create_room"),
"seat_count": 10,
"mode": "voice",
"room_name": "CP Smoke Room",
"room_avatar": "https://cdn.example.com/cp-smoke-room.png",
"room_description": "cp smoke",
})
if err != nil {
return err
}
roomID := jsonString(resp.Data, "room", "room_id")
if roomID == "" {
return fmt.Errorf("create room response missing room.room_id: %s", string(resp.Data))
}
s.roomID = roomID
for _, userID := range []int64{userA, userB, userC} {
if _, err := s.request(ctx, http.MethodPost, "/api/v1/rooms/join", userID, map[string]any{
"room_id": roomID,
"command_id": commandID(fmt.Sprintf("join_%d", userID)),
"role": "audience",
}); err != nil {
return fmt.Errorf("join user %d: %w", userID, err)
}
}
panel, err := s.request(ctx, http.MethodGet, "/api/v1/rooms/"+roomID+"/gift-panel", userA, nil)
if err != nil {
return err
}
if !jsonArrayContainsGift(panel.Data, giftCP) {
return fmt.Errorf("gift panel does not contain %s", giftCP)
}
return nil
}
func (s *smoke) testCreateCPApplication(ctx context.Context) error {
if err := s.cleanupCPFacts(ctx); err != nil {
return err
}
if err := s.sendGift(ctx, userA, userB, giftCP, "t01_cp"); err != nil {
return err
}
app, err := s.waitApplication(ctx, userA, userB, "cp", "pending")
if err != nil {
return err
}
if app.ExpiresAtMS <= time.Now().UTC().Add(23*time.Hour).UnixMilli() {
return fmt.Errorf("application expires_at_ms is not about 24h later: %+v", app)
}
if err := s.waitNotice(ctx, "UserCPApplicationCreated", "tencent_im_c2c", "cp_application_created", userB, "delivered"); err != nil {
return err
}
return s.waitNotice(ctx, "UserCPApplicationCreated", "tencent_im_room", "cp_application_created", userB, "delivered")
}
func (s *smoke) testAcceptApplication(ctx context.Context) error {
app, err := s.waitApplication(ctx, userA, userB, "cp", "pending")
if err != nil {
return err
}
resp, err := s.request(ctx, http.MethodPost, "/api/v1/cp/applications/"+urlPath(app.ApplicationID)+"/accept", userB, nil)
if err != nil {
return err
}
if jsonString(resp.Data, "relationship", "relationship_id") != relationshipID(userA, userB) {
return fmt.Errorf("accept response relationship mismatch: %s", string(resp.Data))
}
if _, err := s.waitRelationship(ctx, userA, userB, "cp", 0); err != nil {
return err
}
if err := s.waitApplicationStatus(ctx, app.ApplicationID, "accepted"); err != nil {
return err
}
if err := s.waitNotice(ctx, "UserCPApplicationAccepted", "tencent_im_c2c", "cp_application_accepted", userA, "delivered"); err != nil {
return err
}
if err := s.waitNotice(ctx, "UserCPApplicationAccepted", "tencent_im_room", "cp_application_accepted", userA, "delivered"); err != nil {
return err
}
return s.waitNotice(ctx, "UserCPRelationshipCreated", "tencent_im_region", "cp_relationship_created", 0, "delivered")
}
func (s *smoke) testIntimacyIncrement(ctx context.Context) error {
before, err := s.waitRelationship(ctx, userA, userB, "cp", 0)
if err != nil {
return err
}
if err := s.sendGift(ctx, userA, userB, giftCP, "t03_cp_again"); err != nil {
return err
}
after, err := s.waitRelationship(ctx, userA, userB, "cp", before.IntimacyValue+10)
if err != nil {
return err
}
if after.IntimacyValue != before.IntimacyValue+10 {
return fmt.Errorf("intimacy mismatch before=%d after=%d", before.IntimacyValue, after.IntimacyValue)
}
return s.waitUserOutbox(ctx, "UserCPRelationshipIntimacyChanged")
}
func (s *smoke) testPairSingleRelation(ctx context.Context) error {
if err := s.cleanupCPFacts(ctx); err != nil {
return err
}
if err := s.sendGift(ctx, userA, userB, giftBrother, "t04_brother"); err != nil {
return err
}
if err := s.sendGift(ctx, userA, userB, giftSister, "t04_sister"); err != nil {
return err
}
brother, err := s.waitApplication(ctx, userA, userB, "brother", "pending")
if err != nil {
return err
}
sister, err := s.waitApplication(ctx, userA, userB, "sister", "pending")
if err != nil {
return err
}
if _, err := s.request(ctx, http.MethodPost, "/api/v1/cp/applications/"+urlPath(brother.ApplicationID)+"/accept", userB, nil); err != nil {
return err
}
if err := s.waitApplicationStatus(ctx, sister.ApplicationID, "blocked"); err != nil {
return err
}
err = s.expectCode(ctx, http.MethodPost, "/api/v1/cp/applications/"+urlPath(sister.ApplicationID)+"/accept", userB, nil, "CONFLICT")
if err != nil {
return err
}
_, err = s.waitRelationship(ctx, userA, userB, "brother", 0)
return err
}
func (s *smoke) testUserSingleRelation(ctx context.Context) error {
if err := s.sendGift(ctx, userC, userA, giftCP, "t05_c_to_a"); err != nil {
return err
}
app, err := s.waitApplication(ctx, userC, userA, "cp", "pending")
if err != nil {
return err
}
if err := s.expectCode(ctx, http.MethodPost, "/api/v1/cp/applications/"+urlPath(app.ApplicationID)+"/accept", userA, nil, "CONFLICT"); err != nil {
return err
}
if _, err := s.waitRelationship(ctx, userA, userB, "brother", 0); err != nil {
return err
}
return s.assertNoRelationship(ctx, userA, userC)
}
func (s *smoke) testExpiredApplication(ctx context.Context) error {
if err := s.cleanupCPFacts(ctx); err != nil {
return err
}
if err := s.sendGift(ctx, userA, userB, giftCP, "t06_expire"); err != nil {
return err
}
app, err := s.waitApplication(ctx, userA, userB, "cp", "pending")
if err != nil {
return err
}
if _, err := s.db.ExecContext(ctx, `
UPDATE hyapp_user.user_cp_applications SET expires_at_ms = ?, updated_at_ms = ?
WHERE app_code = ? AND application_id = ?`, time.Now().UTC().Add(-time.Minute).UnixMilli(), time.Now().UTC().UnixMilli(), appCode, app.ApplicationID,
); err != nil {
return err
}
if err := s.expectCode(ctx, http.MethodPost, "/api/v1/cp/applications/"+urlPath(app.ApplicationID)+"/accept", userB, nil, "CONFLICT"); err != nil {
return err
}
return s.waitApplicationStatus(ctx, app.ApplicationID, "expired")
}
func (s *smoke) testRejectApplication(ctx context.Context) error {
if err := s.cleanupCPFacts(ctx); err != nil {
return err
}
if err := s.sendGift(ctx, userA, userB, giftCP, "t07_reject"); err != nil {
return err
}
app, err := s.waitApplication(ctx, userA, userB, "cp", "pending")
if err != nil {
return err
}
if _, err := s.request(ctx, http.MethodPost, "/api/v1/cp/applications/"+urlPath(app.ApplicationID)+"/reject", userB, map[string]any{"reason": "smoke"}); err != nil {
return err
}
if err := s.waitApplicationStatus(ctx, app.ApplicationID, "rejected"); err != nil {
return err
}
if err := s.assertNoRelationship(ctx, userA, userB); err != nil {
return err
}
if err := s.waitNotice(ctx, "UserCPApplicationRejected", "tencent_im_c2c", "cp_application_rejected", userA, "delivered"); err != nil {
return err
}
return s.waitNotice(ctx, "UserCPApplicationRejected", "tencent_im_room", "cp_application_rejected", userA, "delivered")
}
func (s *smoke) sendGift(ctx context.Context, senderID int64, targetID int64, giftID string, key string) error {
_, err := s.request(ctx, http.MethodPost, "/api/v1/rooms/gift/send", senderID, map[string]any{
"room_id": s.roomID,
"command_id": commandID(key),
"target_type": "user",
"target_user_ids": []int64{targetID},
"gift_id": giftID,
"gift_count": 1,
})
return err
}
func (s *smoke) request(ctx context.Context, method string, path string, userID int64, body any) (envelope, error) {
var reader io.Reader
if body != nil {
raw, err := json.Marshal(body)
if err != nil {
return envelope{}, err
}
reader = bytes.NewReader(raw)
}
req, err := http.NewRequestWithContext(ctx, method, s.gateway+path, reader)
if err != nil {
return envelope{}, err
}
req.Header.Set("Authorization", "Bearer "+tokenForUser(userID))
req.Header.Set("X-App-Code", appCode)
req.Header.Set("Content-Type", "application/json")
resp, err := s.http.Do(req)
if err != nil {
return envelope{}, err
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
var out envelope
if err := json.Unmarshal(raw, &out); err != nil {
return envelope{}, fmt.Errorf("%s %s returned non-envelope status=%d body=%s", method, path, resp.StatusCode, string(raw))
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 || out.Code != "OK" {
return out, fmt.Errorf("%s %s failed status=%d code=%s message=%s body=%s", method, path, resp.StatusCode, out.Code, out.Message, string(raw))
}
return out, nil
}
func (s *smoke) expectCode(ctx context.Context, method string, path string, userID int64, body any, expected string) error {
out, err := s.request(ctx, method, path, userID, body)
if err == nil {
return fmt.Errorf("expected code %s, got OK data=%s", expected, string(out.Data))
}
if out.Code != expected {
return fmt.Errorf("expected code %s, got %s: %v", expected, out.Code, err)
}
return nil
}
func (s *smoke) waitApplication(ctx context.Context, requester int64, target int64, relationType string, status string) (cpApplication, error) {
var app cpApplication
err := wait(ctx, 25*time.Second, func() error {
row := s.db.QueryRowContext(ctx, `
SELECT application_id, relation_type, status, room_id, expires_at_ms
FROM hyapp_user.user_cp_applications
WHERE app_code = ? AND requester_user_id = ? AND target_user_id = ? AND relation_type = ? AND status = ?
ORDER BY updated_at_ms DESC LIMIT 1`,
appCode, requester, target, relationType, status,
)
return row.Scan(&app.ApplicationID, &app.RelationType, &app.Status, &app.RoomID, &app.ExpiresAtMS)
})
return app, err
}
func (s *smoke) waitApplicationStatus(ctx context.Context, applicationID string, status string) error {
return wait(ctx, 20*time.Second, func() error {
var got string
err := s.db.QueryRowContext(ctx, `
SELECT status FROM hyapp_user.user_cp_applications WHERE app_code = ? AND application_id = ?`,
appCode, applicationID,
).Scan(&got)
if err != nil {
return err
}
if got != status {
return fmt.Errorf("application %s status=%s, want %s", applicationID, got, status)
}
return nil
})
}
func (s *smoke) waitRelationship(ctx context.Context, user1 int64, user2 int64, relationType string, minIntimacy int64) (cpRelationship, error) {
var rel cpRelationship
a, b := orderedPair(user1, user2)
err := wait(ctx, 25*time.Second, func() error {
row := s.db.QueryRowContext(ctx, `
SELECT relationship_id, relation_type, status, intimacy_value, level_no
FROM hyapp_user.user_cp_relationships
WHERE app_code = ? AND user_a_id = ? AND user_b_id = ? AND relation_type = ? AND status = 'active'
LIMIT 1`,
appCode, a, b, relationType,
)
if err := row.Scan(&rel.RelationshipID, &rel.RelationType, &rel.Status, &rel.IntimacyValue, &rel.Level); err != nil {
return err
}
if rel.IntimacyValue < minIntimacy {
return fmt.Errorf("relationship intimacy=%d, want >=%d", rel.IntimacyValue, minIntimacy)
}
return nil
})
return rel, err
}
func (s *smoke) assertNoRelationship(ctx context.Context, user1 int64, user2 int64) error {
a, b := orderedPair(user1, user2)
var count int64
if err := s.db.QueryRowContext(ctx, `
SELECT COUNT(*) FROM hyapp_user.user_cp_relationships
WHERE app_code = ? AND user_a_id = ? AND user_b_id = ? AND status = 'active'`,
appCode, a, b,
).Scan(&count); err != nil {
return err
}
if count != 0 {
return fmt.Errorf("unexpected active relationship for %d/%d", user1, user2)
}
return nil
}
func (s *smoke) waitUserOutbox(ctx context.Context, eventType string) error {
return wait(ctx, 20*time.Second, func() error {
var count int64
err := s.db.QueryRowContext(ctx, `
SELECT COUNT(*) FROM hyapp_user.user_outbox
WHERE app_code = ? AND event_type = ? AND created_at_ms >= ?`,
appCode, eventType, s.startMS,
).Scan(&count)
if err != nil {
return err
}
if count == 0 {
return fmt.Errorf("missing user_outbox %s", eventType)
}
return nil
})
}
func (s *smoke) waitNotice(ctx context.Context, sourceEventType string, channel string, noticeType string, targetUserID int64, wantStatus string) error {
return wait(ctx, 35*time.Second, func() error {
query := `
SELECT nde.status, COALESCE(nde.last_error, '')
FROM hyapp_notice.notice_delivery_events nde
JOIN hyapp_user.user_outbox uo
ON uo.app_code = nde.app_code AND uo.event_id = nde.source_event_id
WHERE nde.source_name = 'user_outbox'
AND nde.app_code = ?
AND uo.event_type = ?
AND nde.channel = ?
AND nde.notice_type = ?
AND nde.created_at_ms >= ?`
args := []any{appCode, sourceEventType, channel, noticeType, s.startMS}
if targetUserID > 0 {
query += ` AND nde.target_user_id = ?`
args = append(args, targetUserID)
}
query += ` ORDER BY nde.updated_at_ms DESC LIMIT 1`
var status, lastErr string
err := s.db.QueryRowContext(ctx, query, args...).Scan(&status, &lastErr)
if err != nil {
return err
}
if status != wantStatus {
return fmt.Errorf("notice %s/%s/%s status=%s last_error=%s", sourceEventType, channel, noticeType, status, lastErr)
}
return nil
})
}
func wait(ctx context.Context, timeout time.Duration, fn func() error) error {
deadline := time.Now().Add(timeout)
var last error
for time.Now().Before(deadline) {
if err := fn(); err != nil {
last = err
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(500 * time.Millisecond):
continue
}
}
return nil
}
if last == nil {
last = errors.New("condition not satisfied")
}
return last
}
func tokenForUser(userID int64) string {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": strconv.FormatInt(userID, 10),
"app_code": appCode,
"sid": "cp-smoke-" + strconv.FormatInt(userID, 10),
"profile_completed": true,
"onboarding_status": "completed",
"exp": time.Now().Add(2 * time.Hour).Unix(),
"iat": time.Now().Unix(),
})
signed, err := token.SignedString([]byte(jwtSecret))
if err != nil {
panic(err)
}
return signed
}
func commandID(key string) string {
return fmt.Sprintf("cp-smoke:%s:%d", key, time.Now().UTC().UnixNano())
}
func jsonString(raw json.RawMessage, path ...string) string {
var cur any
if err := json.Unmarshal(raw, &cur); err != nil {
return ""
}
for _, key := range path {
obj, ok := cur.(map[string]any)
if !ok {
return ""
}
cur = obj[key]
}
switch value := cur.(type) {
case string:
return value
case float64:
if value == float64(int64(value)) {
return strconv.FormatInt(int64(value), 10)
}
}
return ""
}
func jsonArrayContainsGift(raw json.RawMessage, giftID string) bool {
var obj map[string]any
if err := json.Unmarshal(raw, &obj); err != nil {
return false
}
items, _ := obj["gifts"].([]any)
for _, item := range items {
gift, _ := item.(map[string]any)
if strings.TrimSpace(fmt.Sprint(gift["gift_id"])) == giftID {
return true
}
}
return false
}
func orderedPair(a int64, b int64) (int64, int64) {
if a < b {
return a, b
}
return b, a
}
func relationshipID(a int64, b int64) string {
x, y := orderedPair(a, b)
return fmt.Sprintf("cp_rel:%d:%d", x, y)
}
func urlPath(value string) string {
return strings.ReplaceAll(value, "/", "%2F")
}
func env(key string, fallback string) string {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value
}
return fallback
}
func envDuration(key string, fallback time.Duration) time.Duration {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
if parsed, err := time.ParseDuration(value); err == nil {
return parsed
}
}
return fallback
}
func isMissingTable(err error) bool {
return strings.Contains(err.Error(), "doesn't exist")
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, "cp smoke failed:", err)
os.Exit(1)
}