2026-06-16 17:00:06 +08:00

351 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 进入 contextRedis 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,
}
}