Merge branch 'main' into codex/admin-refresh-token-test
This commit is contained in:
commit
9c2413ca11
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
@ -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
@ -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);
|
||||
|
||||
@ -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
@ -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 {
|
||||
|
||||
233
docs/CP关系服务端开发文档.md
Normal file
233
docs/CP关系服务端开发文档.md
Normal 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`,失败可重试。
|
||||
119
docs/CP关系本地真实链路测试用例.md
Normal file
119
docs/CP关系本地真实链路测试用例.md
Normal file
@ -0,0 +1,119 @@
|
||||
# CP关系本地真实链路测试用例
|
||||
|
||||
本文只记录本地真实链路验证用例。业务动作优先走 gateway HTTP、room/wallet/user/notice 服务和 RocketMQ;SQL 只用于准备本地测试数据和核对最终事实。
|
||||
|
||||
## 环境前置
|
||||
|
||||
- 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`。
|
||||
455
docs/flutter对接/CP关系Flutter对接.md
Normal file
455
docs/flutter对接/CP关系Flutter对接.md
Normal 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 里按礼物价格推算。
|
||||
179
docs/flutter对接/心跳接口Flutter对接.md
Normal file
179
docs/flutter对接/心跳接口Flutter对接.md
Normal 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` 后停止麦位心跳。
|
||||
@ -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` | 开启或关闭房间公屏 |
|
||||
|
||||
@ -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),
|
||||
|
||||
50
server/admin/internal/modules/cprelation/handler.go
Normal file
50
server/admin/internal/modules/cprelation/handler.go
Normal 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)
|
||||
}
|
||||
15
server/admin/internal/modules/cprelation/routes.go
Normal file
15
server/admin/internal/modules/cprelation/routes.go
Normal 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)
|
||||
}
|
||||
373
server/admin/internal/modules/cprelation/service.go
Normal file
373
server/admin/internal/modules/cprelation/service.go
Normal 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)
|
||||
}
|
||||
61
server/admin/internal/modules/cprelation/service_test.go
Normal file
61
server/admin/internal/modules/cprelation/service_test.go
Normal 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},
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
59
server/admin/migrations/040_cp_config_navigation.sql
Normal file
59
server/admin/migrations/040_cp_config_navigation.sql
Normal 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';
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
})
|
||||
}
|
||||
@ -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 注册表解析 client;gateway 用它把包名转换成内部 app_code。
|
||||
func (h *Handler) SetAppRegistryClient(appRegistryClient client.AppRegistryClient) {
|
||||
h.appRegistryClient = appRegistryClient
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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]
|
||||
}
|
||||
153
services/notice-service/internal/modules/cpnotice/payload.go
Normal file
153
services/notice-service/internal/modules/cpnotice/payload.go
Normal 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
|
||||
}
|
||||
407
services/notice-service/internal/modules/cpnotice/service.go
Normal file
407
services/notice-service/internal/modules/cpnotice/service.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
59
services/notice-service/internal/modules/cpnotice/worker.go
Normal file
59
services/notice-service/internal/modules/cpnotice/worker.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 '升级发放的装扮资源组 ID,0 表示不发放',
|
||||
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',
|
||||
|
||||
@ -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 cache;cache 不作为启动硬依赖。
|
||||
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{
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 风控撤销。
|
||||
|
||||
108
services/user-service/internal/domain/cp/cp.go
Normal file
108
services/user-service/internal/domain/cp/cp.go
Normal 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
|
||||
}
|
||||
@ -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 下已经占用注册设备号的用户。
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,避免签发和下游解析在算法上分叉。
|
||||
|
||||
136
services/user-service/internal/service/cp/service.go
Normal file
136
services/user-service/internal/service/cp/service.go
Normal 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 用例,不直接投递 IM;IM 由 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 ""
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
808
services/user-service/internal/storage/mysql/cp/repository.go
Normal file
808
services/user-service/internal/storage/mysql/cp/repository.go
Normal 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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
180
services/user-service/internal/transport/grpc/cp.go
Normal file
180
services/user-service/internal/transport/grpc/cp.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -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_code,gateway 后续会把 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())
|
||||
|
||||
@ -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 月生成,后续结算按此键定位周期账户。
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
892
tools/cp_flow_smoke/main.go
Normal 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(®ionID)
|
||||
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)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user