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 } // ListCPIntimacyLeaderboard 返回 cron 写入 Redis 的 CP 亲密值排行榜。 func (s *Server) ListCPIntimacyLeaderboard(ctx context.Context, req *userv1.ListCPIntimacyLeaderboardRequest) (*userv1.ListCPIntimacyLeaderboardResponse, error) { // app_code 必须从 meta 进入 context,Redis key 按 app 隔离;gateway 不传时会落到默认 app。 ctx = contextWithApp(ctx, req.GetMeta()) if s.cpSvc == nil { return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp service is not configured")) } // service 层负责关系类型归一、分页夹紧和 Redis 读取;transport 只做 proto 边界转换。 page, err := s.cpSvc.ListIntimacyLeaderboard(ctx, req.GetRelationType(), req.GetPage(), req.GetPageSize()) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &userv1.ListCPIntimacyLeaderboardResponse{ Items: make([]*userv1.CPIntimacyLeaderboardItem, 0, len(page.Items)), Total: page.Total, Page: page.Page, PageSize: page.PageSize, ServerTimeMs: page.ServerTimeMS, } for _, item := range page.Items { // item 内已经包含 cron 固化的双方资料和头像框快照,这里不能再回表或跨服务补字段。 resp.Items = append(resp.Items, toProtoCPIntimacyLeaderboardItem(item)) } return resp, nil } // PrepareBreakCPRelationship 创建解除关系占位;gateway 随后按返回费用调用 wallet-service 扣金币。 func (s *Server) PrepareBreakCPRelationship(ctx context.Context, req *userv1.PrepareBreakCPRelationshipRequest) (*userv1.PrepareBreakCPRelationshipResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) if s.cpSvc == nil { return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp service is not configured")) } breakup, relationship, err := s.cpSvc.PrepareBreakRelationship(ctx, req.GetUserId(), req.GetRelationshipId(), req.GetCommandId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.PrepareBreakCPRelationshipResponse{ Relationship: toProtoCPRelationship(relationship), CommandId: breakup.CommandID, Status: breakup.Status, BreakupCostCoins: breakup.BreakupCostCoins, WalletTransactionId: breakup.WalletTransactionID, CoinBalanceAfter: breakup.CoinBalanceAfter, }, nil } // ConfirmBreakCPRelationship 在钱包扣费成功后确认解除,关系状态和 outbox 在同一事务提交。 func (s *Server) ConfirmBreakCPRelationship(ctx context.Context, req *userv1.ConfirmBreakCPRelationshipRequest) (*userv1.ConfirmBreakCPRelationshipResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) if s.cpSvc == nil { return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp service is not configured")) } breakup, relationship, err := s.cpSvc.ConfirmBreakRelationship(ctx, req.GetUserId(), req.GetRelationshipId(), req.GetCommandId(), req.GetWalletTransactionId(), req.GetPaidCoinAmount(), req.GetCoinBalanceAfter()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.ConfirmBreakCPRelationshipResponse{ Relationship: toProtoCPRelationship(relationship), BreakupCostCoins: breakup.BreakupCostCoins, WalletTransactionId: breakup.WalletTransactionID, CoinBalanceAfter: breakup.CoinBalanceAfter, }, nil } // CancelBreakCPRelationship 取消还未扣费成功的解除占位,避免钱包失败后关系被其他请求长期阻塞。 func (s *Server) CancelBreakCPRelationship(ctx context.Context, req *userv1.CancelBreakCPRelationshipRequest) (*userv1.CancelBreakCPRelationshipResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) if s.cpSvc == nil { return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp service is not configured")) } breakup, err := s.cpSvc.CancelBreakRelationship(ctx, req.GetUserId(), req.GetRelationshipId(), req.GetCommandId(), req.GetReason()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.CancelBreakCPRelationshipResponse{ CommandId: breakup.CommandID, Status: breakup.Status, }, 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 } // ListCPWeeklyRankEntries 给 activity-service 读取周榜结算快照;该 RPC 不暴露给 App,避免 H5 直接扫 MySQL 周榜事实。 func (s *Server) ListCPWeeklyRankEntries(ctx context.Context, req *userv1.ListCPWeeklyRankEntriesRequest) (*userv1.ListCPWeeklyRankEntriesResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) if s.cpSvc == nil { return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp service is not configured")) } entries, serverTimeMS, err := s.cpSvc.ListWeeklyRankEntries(ctx, req.GetRelationType(), req.GetStartMs(), req.GetEndMs(), req.GetLimit()) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &userv1.ListCPWeeklyRankEntriesResponse{ Entries: make([]*userv1.CPWeeklyRankEntry, 0, len(entries)), ServerTimeMs: serverTimeMS, } for _, entry := range entries { resp.Entries = append(resp.Entries, toProtoCPWeeklyRankEntry(entry)) } return resp, 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, CurrentLevelThreshold: relationship.CurrentLevelThreshold, NextLevelThreshold: relationship.NextLevelThreshold, NeededForNextLevel: relationship.NeededForNextLevel, LevelProgressPercent: relationship.LevelProgressPercent, MaxLevel: relationship.MaxLevel, BreakupCostCoins: relationship.BreakupCostCoins, EndedAtMs: relationship.EndedAtMS, } } 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 toProtoCPWeeklyRankEntry(item cpdomain.WeeklyRankEntry) *userv1.CPWeeklyRankEntry { if item.RelationshipID == "" { return nil } return &userv1.CPWeeklyRankEntry{ Rank: item.Rank, RelationshipId: item.RelationshipID, RelationType: item.RelationType, Score: item.Score, UserA: toProtoCPIntimacyLeaderboardUser(item.UserA), UserB: toProtoCPIntimacyLeaderboardUser(item.UserB), FormedAtMs: item.FormedAtMS, UpdatedAtMs: item.UpdatedAtMS, FirstScoredAtMs: item.FirstScoredAtMS, LastScoredAtMs: item.LastScoredAtMS, } } func toProtoCPIntimacyLeaderboardItem(item cpdomain.IntimacyLeaderboardEntry) *userv1.CPIntimacyLeaderboardItem { if item.RelationshipID == "" { return nil } return &userv1.CPIntimacyLeaderboardItem{ Rank: item.Rank, RelationshipId: item.RelationshipID, RelationType: item.RelationType, IntimacyValue: item.IntimacyValue, Level: item.Level, UserA: toProtoCPIntimacyLeaderboardUser(item.UserA), UserB: toProtoCPIntimacyLeaderboardUser(item.UserB), FormedAtMs: item.FormedAtMS, UpdatedAtMs: item.UpdatedAtMS, } } func toProtoCPIntimacyLeaderboardUser(user cpdomain.IntimacyLeaderboardUser) *userv1.CPIntimacyLeaderboardUser { if user.UserID <= 0 { return nil } return &userv1.CPIntimacyLeaderboardUser{ UserId: user.UserID, DisplayUserId: user.DisplayUserID, Username: user.Username, Avatar: user.Avatar, AvatarFrame: toProtoCPAvatarFrame(user.AvatarFrame), } } func toProtoCPAvatarFrame(frame *cpdomain.AvatarFrameSnapshot) *userv1.CPAvatarFrameSnapshot { if frame == nil || frame.ResourceID <= 0 { return nil } return &userv1.CPAvatarFrameSnapshot{ ResourceId: frame.ResourceID, ResourceCode: frame.ResourceCode, Name: frame.Name, AssetUrl: frame.AssetURL, PreviewUrl: frame.PreviewURL, AnimationUrl: frame.AnimationURL, MetadataJson: frame.MetadataJSON, } } 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, } }