// Package grpc 将 user-service 认证、用户和短号用例适配成 protobuf gRPC 服务。 package grpc import ( userv1 "hyapp.local/api/proto/user/v1" authdomain "hyapp/services/user-service/internal/domain/auth" hostdomain "hyapp/services/user-service/internal/domain/host" invitedomain "hyapp/services/user-service/internal/domain/invite" mictimedomain "hyapp/services/user-service/internal/domain/mictime" userdomain "hyapp/services/user-service/internal/domain/user" authservice "hyapp/services/user-service/internal/service/auth" ) // toProtoApp 把 App 注册表事实转换为 protobuf 投影。 func toProtoApp(app userdomain.App) *userv1.App { return &userv1.App{ AppId: app.AppID, AppCode: app.AppCode, AppName: app.AppName, PackageName: app.PackageName, Platform: app.Platform, Status: app.Status, CreatedAtMs: app.CreatedAtMs, UpdatedAtMs: app.UpdatedAtMs, } } // toProtoToken 把领域令牌转换为 protobuf 响应。 func toProtoToken(token authdomain.Token) *userv1.AuthToken { // Token 中包含 refresh token 原文,只能出现在登录/刷新响应路径。 return &userv1.AuthToken{ AppCode: token.AppCode, UserId: token.UserID, DisplayUserId: token.DisplayUserID, DefaultDisplayUserId: token.DefaultDisplayUserID, DisplayUserIdKind: token.DisplayUserIDKind, DisplayUserIdExpiresAtMs: token.DisplayUserIDExpiresAtMs, ProfileCompleted: token.ProfileCompleted, OnboardingStatus: token.OnboardingStatus, SessionId: token.SessionID, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, ExpiresInSec: token.ExpiresInSec, TokenType: token.TokenType, } } // toProtoUser 把领域用户转换为 protobuf 用户投影。 func toProtoUser(user userdomain.User) *userv1.User { // User proto 只暴露用户主状态和当前展示短号,不包含密码、三方身份或房间状态。 return &userv1.User{ AppCode: user.AppCode, UserId: user.UserID, Status: toProtoStatus(user.Status), CreatedAtMs: user.CreatedAtMs, UpdatedAtMs: user.UpdatedAtMs, DisplayUserId: user.CurrentDisplayUserID, DefaultDisplayUserId: user.DefaultDisplayUserID, DisplayUserIdKind: string(user.CurrentDisplayUserIDKind), DisplayUserIdExpiresAtMs: user.CurrentDisplayUserIDExpiresAtMs, Username: user.Username, Gender: user.Gender, Country: user.Country, Avatar: user.Avatar, Birth: user.BirthDate, Language: user.Language, CountryId: user.CountryID, CountryName: user.CountryName, CountryDisplayName: user.CountryDisplayName, RegionId: user.RegionID, RegionCode: user.RegionCode, RegionName: user.RegionName, ProfileCompleted: user.ProfileCompleted, ProfileCompletedAtMs: user.ProfileCompletedAtMs, OnboardingStatus: string(user.OnboardingStatus), IsoNumeric: user.ISONumeric, PhoneCountryCode: user.PhoneCountryCode, CountryEnabled: user.CountryEnabled, Invite: toProtoInviteOverview(user.InviteOverview), } } func toProtoBusinessUserLookupItem(item userdomain.BusinessUserLookupItem) *userv1.BusinessUserLookupItem { return &userv1.BusinessUserLookupItem{ UserId: item.UserID, DisplayUserId: item.DisplayUserID, Username: item.Username, Avatar: item.Avatar, Status: toProtoStatus(item.Status), RegionId: item.RegionID, RegionCode: item.RegionCode, RegionName: item.RegionName, } } func toProtoInviteOverview(overview invitedomain.Overview) *userv1.InviteOverview { if overview.MyInviteCode == "" && !overview.InviteEnabled && overview.InviteCount == 0 && overview.ValidInviteCount == 0 && overview.ValidInviteThresholdCoin == 0 { return nil } return &userv1.InviteOverview{ MyInviteCode: overview.MyInviteCode, InviteEnabled: overview.InviteEnabled, InviteCount: overview.InviteCount, ValidInviteCount: overview.ValidInviteCount, ValidInviteThresholdCoin: overview.ValidInviteThresholdCoin, } } func toProtoInviteBinding(binding invitedomain.Binding) *userv1.InviteBinding { if !binding.Bound && binding.InviteCode == "" && binding.InviterUserID == 0 { return nil } return &userv1.InviteBinding{ Bound: binding.Bound, InviteCode: binding.InviteCode, InviterUserId: binding.InviterUserID, } } func toProtoMicLifetimeStats(stats mictimedomain.LifetimeStats) *userv1.UserMicLifetimeStats { return &userv1.UserMicLifetimeStats{ AppCode: stats.AppCode, UserId: stats.UserID, SeatOccupiedMs: stats.SeatOccupiedMs, MicOnlineMs: stats.MicOnlineMs, SessionCount: stats.SessionCount, FirstMicAtMs: stats.FirstMicAtMs, LastMicAtMs: stats.LastMicAtMs, UpdatedAtMs: stats.UpdatedAtMs, } } func toProtoProfileStats(stats userdomain.ProfileStats) *userv1.UserProfileStats { return &userv1.UserProfileStats{ AppCode: stats.AppCode, UserId: stats.UserID, VisitorsCount: stats.VisitorsCount, FollowingCount: stats.FollowingCount, FriendsCount: stats.FriendsCount, UpdatedAtMs: stats.UpdatedAtMs, } } func toProtoProfileVisitRecord(record userdomain.ProfileVisitRecord) *userv1.ProfileVisitRecord { return &userv1.ProfileVisitRecord{ VisitorUserId: record.VisitorUserID, TargetUserId: record.TargetUserID, VisitCount: record.VisitCount, LastVisitedAtMs: record.LastVisitedAtMs, } } func toProtoFollowRecord(record userdomain.FollowRecord) *userv1.FollowRecord { return &userv1.FollowRecord{ FollowerUserId: record.FollowerUserID, FolloweeUserId: record.FolloweeUserID, FollowedAtMs: record.FollowedAtMs, } } func toProtoFriendRecord(record userdomain.FriendRecord) *userv1.FriendRecord { return &userv1.FriendRecord{ UserId: record.UserID, FriendUserId: record.FriendUserID, FriendedAtMs: record.FriendedAtMs, } } func toProtoFriendApplication(application userdomain.FriendApplication) *userv1.FriendApplication { return &userv1.FriendApplication{ RequesterUserId: application.RequesterUserID, TargetUserId: application.TargetUserID, Status: application.Status, CreatedAtMs: application.CreatedAtMs, UpdatedAtMs: application.UpdatedAtMs, } } // toProtoCountry 把国家领域对象转换为管理 RPC 响应。 func toProtoCountry(country userdomain.Country) *userv1.Country { return &userv1.Country{ AppCode: country.AppCode, CountryId: country.CountryID, CountryName: country.CountryName, CountryCode: country.CountryCode, CountryDisplayName: country.CountryDisplayName, SortOrder: country.SortOrder, CreatedAtMs: country.CreatedAtMs, UpdatedAtMs: country.UpdatedAtMs, IsoAlpha3: country.ISOAlpha3, IsoNumeric: country.ISONumeric, PhoneCountryCode: country.PhoneCountryCode, Enabled: country.Enabled, Flag: country.Flag, } } // toProtoRegion 把区域领域对象转换为管理 RPC 响应。 func toProtoRegion(region userdomain.Region) *userv1.Region { return &userv1.Region{ AppCode: region.AppCode, RegionId: region.RegionID, RegionCode: region.RegionCode, Name: region.Name, Status: region.Status, Countries: append([]string{}, region.Countries...), SortOrder: region.SortOrder, CreatedAtMs: region.CreatedAtMs, UpdatedAtMs: region.UpdatedAtMs, } } // toProtoIdentity 把短号领域投影转换为 protobuf 响应。 func toProtoIdentity(identity userdomain.Identity) *userv1.UserIdentity { // Identity proto 用于短号解析和修改结果,字段语义与领域 Identity 保持一致。 return &userv1.UserIdentity{ AppCode: identity.AppCode, UserId: identity.UserID, DisplayUserId: identity.DisplayUserID, Status: string(identity.Status), DefaultDisplayUserId: identity.DefaultDisplayUserID, DisplayUserIdKind: string(identity.DisplayUserIDKind), DisplayUserIdExpiresAtMs: identity.DisplayUserIDExpiresAtMs, } } // toProtoStatus 固定未知状态的安全默认值。 func toProtoStatus(status userdomain.Status) userv1.UserStatus { // 状态枚举转换集中在这里,未知状态使用 UNSPECIFIED 安全兜底。 switch status { case userdomain.StatusActive: return userv1.UserStatus_USER_STATUS_ACTIVE case userdomain.StatusDisabled: return userv1.UserStatus_USER_STATUS_DISABLED case userdomain.StatusBanned: return userv1.UserStatus_USER_STATUS_BANNED default: return userv1.UserStatus_USER_STATUS_UNSPECIFIED } } // authMeta 提取认证链路审计所需的 gateway 透传字段。 func authMeta(meta *userv1.RequestMeta) authservice.Meta { if meta == nil { // meta 可选,缺失时审计字段为空但不能让 RPC panic。 return authservice.Meta{} } // 只提取认证审计需要的字段,device_id 在各 RPC 中单独传入。 return authservice.Meta{ AppCode: meta.GetAppCode(), RequestID: meta.GetRequestId(), ClientIP: meta.GetClientIp(), UserAgent: meta.GetUserAgent(), CountryByIP: meta.GetCountryByIp(), Platform: meta.GetPlatform(), Language: meta.GetLanguage(), Timezone: meta.GetTimezone(), } } // toProtoHostProfile 把 host profile 事实转换为内部 gRPC 投影。 func toProtoHostProfile(profile hostdomain.HostProfile) *userv1.HostProfile { if profile.UserID == 0 { return nil } return &userv1.HostProfile{ UserId: profile.UserID, Status: profile.Status, RegionId: profile.RegionID, CurrentAgencyId: profile.CurrentAgencyID, CurrentMembershipId: profile.CurrentMembershipID, Source: profile.Source, FirstBecameHostAtMs: profile.FirstBecameHostAtMs, CreatedAtMs: profile.CreatedAtMs, UpdatedAtMs: profile.UpdatedAtMs, } } // toProtoAgency 把 Agency 事实转换为内部 gRPC 投影。 func toProtoAgency(agency hostdomain.Agency) *userv1.Agency { if agency.AgencyID == 0 { return nil } return &userv1.Agency{ AgencyId: agency.AgencyID, OwnerUserId: agency.OwnerUserID, RegionId: agency.RegionID, ParentBdUserId: agency.ParentBDUserID, Name: agency.Name, Status: agency.Status, JoinEnabled: agency.JoinEnabled, MaxHosts: agency.MaxHosts, CreatedByUserId: agency.CreatedByUserID, CreatedAtMs: agency.CreatedAtMs, UpdatedAtMs: agency.UpdatedAtMs, } } // toProtoBDProfile 把 BD profile 事实转换为内部 gRPC 投影。 func toProtoBDProfile(profile hostdomain.BDProfile) *userv1.BDProfile { if profile.UserID == 0 { return nil } return &userv1.BDProfile{ UserId: profile.UserID, Role: profile.Role, RegionId: profile.RegionID, ParentLeaderUserId: profile.ParentLeaderUserID, Status: profile.Status, CreatedByUserId: profile.CreatedByUserID, CreatedAtMs: profile.CreatedAtMs, UpdatedAtMs: profile.UpdatedAtMs, } } // toProtoCoinSellerProfile 把币商身份事实转换为内部 gRPC 投影。 func toProtoCoinSellerProfile(profile hostdomain.CoinSellerProfile) *userv1.CoinSellerProfile { if profile.UserID == 0 { return nil } return &userv1.CoinSellerProfile{ UserId: profile.UserID, Status: profile.Status, MerchantAssetType: profile.MerchantAssetType, CreatedByUserId: profile.CreatedByUserID, CreatedAtMs: profile.CreatedAtMs, UpdatedAtMs: profile.UpdatedAtMs, } } // toProtoUserRoleSummary 把 host domain 入口显隐摘要转换为 gRPC 投影。 func toProtoUserRoleSummary(summary hostdomain.UserRoleSummary) *userv1.UserRoleSummary { if summary.UserID == 0 { return nil } return &userv1.UserRoleSummary{ UserId: summary.UserID, IsHost: summary.IsHost, IsAgency: summary.IsAgency, IsBd: summary.IsBD, IsBdLeader: summary.IsBDLeader, IsCoinSeller: summary.IsCoinSeller, HostStatus: summary.HostStatus, AgencyId: summary.AgencyID, BdId: summary.BDID, BdStatus: summary.BDStatus, CoinSellerStatus: summary.CoinSellerStatus, PendingRoleInvitations: summary.PendingRoleInvitations, } } // toProtoAgencyMembership 把 Agency membership 事实转换为内部 gRPC 投影。 func toProtoAgencyMembership(membership hostdomain.AgencyMembership) *userv1.AgencyMembership { if membership.MembershipID == 0 { return nil } return &userv1.AgencyMembership{ MembershipId: membership.MembershipID, AgencyId: membership.AgencyID, HostUserId: membership.HostUserID, RegionId: membership.RegionID, MembershipType: membership.MembershipType, Status: membership.Status, JoinedAtMs: membership.JoinedAtMs, EndedAtMs: membership.EndedAtMs, EndedByUserId: membership.EndedByUserID, EndedReason: membership.EndedReason, CreatedAtMs: membership.CreatedAtMs, UpdatedAtMs: membership.UpdatedAtMs, } } // toProtoAgencyApplication 把 Agency application 事实转换为内部 gRPC 投影。 func toProtoAgencyApplication(application hostdomain.AgencyApplication) *userv1.AgencyApplication { if application.ApplicationID == 0 { return nil } return &userv1.AgencyApplication{ ApplicationId: application.ApplicationID, CommandId: application.CommandID, ApplicantUserId: application.ApplicantUserID, AgencyId: application.AgencyID, RegionId: application.RegionID, Status: application.Status, ReviewedByUserId: application.ReviewedByUserID, ReviewReason: application.ReviewReason, ReviewedAtMs: application.ReviewedAtMs, CreatedAtMs: application.CreatedAtMs, UpdatedAtMs: application.UpdatedAtMs, } } // toProtoRoleInvitation 把角色邀请事实转换为内部 gRPC 投影。 func toProtoRoleInvitation(invitation hostdomain.RoleInvitation) *userv1.RoleInvitation { if invitation.InvitationID == 0 { return nil } return &userv1.RoleInvitation{ InvitationId: invitation.InvitationID, CommandId: invitation.CommandID, InvitationType: invitation.InvitationType, Status: invitation.Status, InviterUserId: invitation.InviterUserID, InviterBdUserId: invitation.InviterBDUserID, TargetUserId: invitation.TargetUserID, RegionId: invitation.RegionID, AgencyName: invitation.AgencyName, ParentBdUserId: invitation.ParentBDUserID, ParentLeaderUserId: invitation.ParentLeaderUserID, ProcessedByUserId: invitation.ProcessedByUserID, ProcessReason: invitation.ProcessReason, ProcessedAtMs: invitation.ProcessedAtMs, CreatedAtMs: invitation.CreatedAtMs, UpdatedAtMs: invitation.UpdatedAtMs, } }