// 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" userservice "hyapp/services/user-service/internal/service/user" ) // 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, } } func toProtoRegisterRiskConfig(config authdomain.RegisterRiskConfig) *userv1.RegisterRiskConfig { return &userv1.RegisterRiskConfig{ AppCode: config.AppCode, MaxAccountsPerDevice: config.MaxAccountsPerDevice, UpdatedAtMs: config.UpdatedAtMs, UpdatedByAdminId: config.UpdatedByAdminID, } } // 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, ProfileBgImg: user.ProfileBgImg, ContactInfo: user.ContactInfo, WithdrawUsdtTrc20Address: user.WithdrawUSDTTRC20Address, 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), PrettyId: user.PrettyID, PrettyDisplayUserId: user.PrettyDisplayUserID, } } 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 toProtoManagerUserBlock(block userservice.ManagerUserBlock) *userv1.ManagerUserBlock { // 经理封禁记录是 H5 弹窗的列表数据;这里仅暴露可展示和可操作的稳定字段。 return &userv1.ManagerUserBlock{ BlockId: block.BlockID, ManagerUserId: block.ManagerUserID, TargetUserId: block.TargetUserID, TargetDisplayUserId: block.TargetDisplayUserID, TargetUsername: block.TargetUsername, TargetAvatar: block.TargetAvatar, Country: block.Country, BlockedUntilMs: block.BlockedUntilMS, Status: block.Status, Reason: block.Reason, CreatedAtMs: block.CreatedAtMS, UpdatedAtMs: block.UpdatedAtMS, } } 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 toProtoInviteAttribution(attribution invitedomain.Attribution) *userv1.InviteAttribution { return &userv1.InviteAttribution{ Found: attribution.Found, AppCode: attribution.AppCode, InvitedUserId: attribution.InvitedUserID, InviterUserId: attribution.InviterUserID, InviteCode: attribution.InviteCode, BoundAtMs: attribution.BoundAtMS, ValidityStatus: attribution.ValidityStatus, AccumulatedRechargeCoinAmount: attribution.AccumulatedRechargeCoinAmount, RequiredRechargeCoinAmount: attribution.RequiredRechargeCoinAmount, ValidAtMs: attribution.ValidAtMS, } } 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, } } func toProtoUserReport(report userdomain.Report) *userv1.UserReport { return &userv1.UserReport{ ReportId: report.ReportID, ReporterUserId: report.ReporterUserID, TargetType: report.TargetType, UserId: report.UserID, RoomId: report.RoomID, ReportType: report.ReportType, Reason: report.Reason, ImageUrls: append([]string{}, report.ImageURLs...), Status: report.Status, CreatedAtMs: report.CreatedAtMs, } } // 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 保持一致。 // PrettyId/PrettyDisplayUserId 只表示当前 active 靓号,没有靓号时为空,不回填默认短号。 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, PrettyId: identity.PrettyID, PrettyDisplayUserId: identity.PrettyDisplayUserID, } } func toProtoPrettyDisplayIDPool(pool userdomain.PrettyDisplayIDPool) *userv1.PrettyDisplayIDPool { // 池配置是后台和 App 可申请列表共用的展示信息,等级轨道和区间必须原样透出。 return &userv1.PrettyDisplayIDPool{ AppCode: pool.AppCode, PoolId: pool.PoolID, Name: pool.Name, LevelTrack: pool.LevelTrack, MinLevel: pool.MinLevel, MaxLevel: pool.MaxLevel, RuleType: pool.RuleType, RuleConfigJson: pool.RuleConfigJSON, Status: pool.Status, SortOrder: pool.SortOrder, CreatedByAdminId: pool.CreatedByAdminID, UpdatedByAdminId: pool.UpdatedByAdminID, CreatedAtMs: pool.CreatedAtMs, UpdatedAtMs: pool.UpdatedAtMs, } } func toProtoPrettyDisplayID(item userdomain.PrettyDisplayID) *userv1.PrettyDisplayID { // 后台发放号码没有 pool_id,Pool 会是零值;前端以 source 判断是否展示池信息。 return &userv1.PrettyDisplayID{ AppCode: item.AppCode, PrettyId: item.PrettyID, PoolId: item.PoolID, Source: item.Source, DisplayUserId: item.DisplayUserID, Status: item.Status, AssignedUserId: item.AssignedUserID, AssignedLeaseId: item.AssignedLeaseID, AssignedAtMs: item.AssignedAtMs, ReleasedAtMs: item.ReleasedAtMs, ReleaseReason: item.ReleaseReason, GeneratedBatchId: item.GeneratedBatchID, CreatedByAdminId: item.CreatedByAdminID, CreatedAtMs: item.CreatedAtMs, UpdatedAtMs: item.UpdatedAtMs, Pool: toProtoPrettyDisplayIDPool(item.Pool), } } func toProtoPrettyDisplayIDBatch(batch userdomain.PrettyDisplayIDGenerationBatch) *userv1.PrettyDisplayIDGenerationBatch { // 批次响应保留 requested/generated/skipped 三个计数,后台可以直接解释生成不足是否来自冲突。 return &userv1.PrettyDisplayIDGenerationBatch{ AppCode: batch.AppCode, BatchId: batch.BatchID, PoolId: batch.PoolID, RuleType: batch.RuleType, RuleConfigJson: batch.RuleConfigJSON, RequestedCount: batch.RequestedCount, GeneratedCount: batch.GeneratedCount, SkippedConflictCount: batch.SkippedConflictCount, Status: batch.Status, OperatorAdminId: batch.OperatorAdminID, RequestId: batch.RequestID, CreatedAtMs: batch.CreatedAtMs, UpdatedAtMs: batch.UpdatedAtMs, } } // 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 } } func fromProtoStatus(status userv1.UserStatus) userdomain.Status { switch status { case userv1.UserStatus_USER_STATUS_ACTIVE: return userdomain.StatusActive case userv1.UserStatus_USER_STATUS_DISABLED: return userdomain.StatusDisabled case userv1.UserStatus_USER_STATUS_BANNED: return userdomain.StatusBanned default: return "" } } // 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, CurrentAgencyOwnerUserId: profile.CurrentAgencyOwnerUserID, 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, Avatar: agency.Avatar, HostCount: agency.ActiveHostCount, } } // 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, CanManageSubCoinSellers: profile.CanManageSubCoinSellers, CreatedByUserId: profile.CreatedByUserID, CreatedAtMs: profile.CreatedAtMs, UpdatedAtMs: profile.UpdatedAtMs, } } // toProtoCoinSellerListItem 把区域币商列表项转换为 App 读取投影。 func toProtoCoinSellerListItem(item hostdomain.CoinSellerListItem) *userv1.CoinSellerListItem { if item.UserID == 0 { return nil } return &userv1.CoinSellerListItem{ UserId: item.UserID, DisplayUserId: item.DisplayUserID, Username: item.Username, Avatar: item.Avatar, CountryId: item.CountryID, CountryCode: item.CountryCode, CountryName: item.CountryName, CountryDisplayName: item.CountryDisplayName, RegionId: item.RegionID, RegionCode: item.RegionCode, RegionName: item.RegionName, Status: item.Status, MerchantAssetType: item.MerchantAssetType, ContactInfo: item.ContactInfo, CanManageSubCoinSellers: item.CanManageSubCoinSellers, UpdatedAtMs: item.UpdatedAtMs, } } // toProtoCoinSellerSubRelation 把父子币商关系事实转换为内部 gRPC 投影。 func toProtoCoinSellerSubRelation(relation hostdomain.CoinSellerSubRelation) *userv1.CoinSellerSubRelation { if relation.RelationID == 0 { return nil } return &userv1.CoinSellerSubRelation{ RelationId: relation.RelationID, ParentUserId: relation.ParentUserID, ChildUserId: relation.ChildUserID, Status: relation.Status, CreatedByUserId: relation.CreatedByUserID, CreatedAtMs: relation.CreatedAtMs, UpdatedAtMs: relation.UpdatedAtMs, } } // toProtoCoinSellerSubApplication 把子币商审核申请转换为内部 gRPC 投影。 func toProtoCoinSellerSubApplication(application hostdomain.CoinSellerSubApplication) *userv1.CoinSellerSubApplication { if application.ApplicationID == 0 { return nil } return &userv1.CoinSellerSubApplication{ ApplicationId: application.ApplicationID, CommandId: application.CommandID, ParentUserId: application.ParentUserID, TargetUserId: application.TargetUserID, Status: application.Status, RelationId: application.RelationID, ReviewedByAdminId: application.ReviewedByAdminID, ReviewReason: application.ReviewReason, ReviewedAtMs: application.ReviewedAtMs, CreatedAtMs: application.CreatedAtMs, UpdatedAtMs: application.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, IsManager: summary.IsManager, 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, } }