// Package grpc 将 user-service 认证、用户和短号用例适配成 protobuf gRPC 服务。 package grpc import ( "context" userv1 "hyapp.local/api/proto/user/v1" "hyapp/pkg/appcode" "hyapp/pkg/xerr" authdomain "hyapp/services/user-service/internal/domain/auth" userdomain "hyapp/services/user-service/internal/domain/user" authservice "hyapp/services/user-service/internal/service/auth" hostservice "hyapp/services/user-service/internal/service/host" userservice "hyapp/services/user-service/internal/service/user" ) // Server 把 user-service 用例层适配为 protobuf gRPC 接口。 type Server struct { // UnimplementedAuthServiceServer 提供未实现 RPC 的默认返回。 userv1.UnimplementedAuthServiceServer // UnimplementedUserServiceServer 提供未实现 RPC 的默认返回。 userv1.UnimplementedUserServiceServer // UnimplementedAppRegistryServiceServer 提供 App 注册表 RPC 的默认返回。 userv1.UnimplementedAppRegistryServiceServer // UnimplementedUserIdentityServiceServer 提供未实现 RPC 的默认返回。 userv1.UnimplementedUserIdentityServiceServer // UnimplementedCountryAdminServiceServer 提供未实现 RPC 的默认返回。 userv1.UnimplementedCountryAdminServiceServer // UnimplementedCountryQueryServiceServer 提供注册页国家查询 RPC 的前向兼容默认返回。 userv1.UnimplementedCountryQueryServiceServer // UnimplementedRegionAdminServiceServer 提供未实现 RPC 的默认返回。 userv1.UnimplementedRegionAdminServiceServer // UnimplementedUserHostServiceServer 提供 host domain RPC 的默认返回。 userv1.UnimplementedUserHostServiceServer // UnimplementedUserHostAdminServiceServer 提供 host 后台关系管理 RPC 的默认返回。 userv1.UnimplementedUserHostAdminServiceServer // authSvc 承载登录、设置密码、refresh 和 logout。 authSvc *authservice.Service // userSvc 承载用户主数据和 display_user_id/靓号用例。 userSvc *userservice.Service // hostSvc 承载 user-service 内 host/Agency/BD 事实用例。 hostSvc *hostservice.Service } // NewServer 创建 user-service gRPC server。 func NewServer(authSvc *authservice.Service, userSvc *userservice.Service, hostSvc ...*hostservice.Service) *Server { // gRPC 层只做协议转换和错误转换,不保存领域状态。 server := &Server{ authSvc: authSvc, userSvc: userSvc, } if len(hostSvc) > 0 { server.hostSvc = hostSvc[0] } return server } // 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()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.ResolveAppResponse{App: toProtoApp(app)}, nil } // LoginPassword 统一把短号不存在、未设置密码和密码错误留给 service 层映射为 AUTH_FAILED。 func (s *Server) LoginPassword(ctx context.Context, req *userv1.LoginPasswordRequest) (*userv1.AuthResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // device_id、request_id、client_ip 等审计字段统一从 RequestMeta 转换。 token, err := s.authSvc.LoginPassword(ctx, req.GetDisplayUserId(), req.GetPassword(), req.GetMeta().GetDeviceId(), authMeta(req.GetMeta())) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.AuthResponse{Token: toProtoToken(token), ProfileCompleted: token.ProfileCompleted, OnboardingStatus: token.OnboardingStatus}, nil } // LoginThirdParty 只接受 provider 凭证,不在 gateway 泄漏 provider secret。 func (s *Server) LoginThirdParty(ctx context.Context, req *userv1.LoginThirdPartyRequest) (*userv1.AuthResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) registration := authdomain.ThirdPartyRegistration{ Username: req.GetUsername(), Gender: req.GetGender(), Country: req.GetCountry(), InviteCode: req.GetInviteCode(), IP: req.GetMeta().GetClientIp(), DeviceID: req.GetDeviceId(), Device: req.GetDevice(), OSVersion: req.GetOsVersion(), Avatar: req.GetAvatar(), BirthDate: req.GetBirth(), AppVersion: req.GetAppVersion(), BuildNumber: req.GetBuildNumber(), Source: req.GetSource(), InstallChannel: req.GetInstallChannel(), Campaign: req.GetCampaign(), Platform: req.GetPlatform(), Language: req.GetLanguage(), Timezone: req.GetTimezone(), } // 是否新用户由 auth service 根据 provider subject 绑定是否存在决定。 token, isNewUser, err := s.authSvc.LoginThirdParty(ctx, req.GetProvider(), req.GetCredential(), registration, authMeta(req.GetMeta())) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.AuthResponse{Token: toProtoToken(token), IsNewUser: isNewUser, ProfileCompleted: token.ProfileCompleted, OnboardingStatus: token.OnboardingStatus}, nil } // SetPassword 为已登录用户首次写入密码身份。 func (s *Server) SetPassword(ctx context.Context, req *userv1.SetPasswordRequest) (*userv1.SetPasswordResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // user_id 必须由 gateway 鉴权后传入,service 层会拒绝非法值。 if err := s.authSvc.SetPassword(ctx, req.GetUserId(), req.GetPassword(), authMeta(req.GetMeta())); err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.SetPasswordResponse{PasswordSet: true}, nil } // RefreshToken 执行 refresh token 轮换。 func (s *Server) RefreshToken(ctx context.Context, req *userv1.RefreshTokenRequest) (*userv1.RefreshTokenResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // refresh token 轮换后响应里会返回新的 refresh token,旧 token 立即失效。 token, err := s.authSvc.RefreshToken(ctx, req.GetRefreshToken(), req.GetMeta().GetDeviceId(), authMeta(req.GetMeta())) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.RefreshTokenResponse{Token: toProtoToken(token)}, nil } // Logout 失效指定会话或 refresh token。 func (s *Server) Logout(ctx context.Context, req *userv1.LogoutRequest) (*userv1.LogoutResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // Logout 支持按 session_id 或 refresh token 失效会话。 revoked, err := s.authSvc.Logout(ctx, req.GetSessionId(), req.GetRefreshToken(), authMeta(req.GetMeta())) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.LogoutResponse{Revoked: revoked}, nil } // GetUser 返回用户主状态。 func (s *Server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // 用户查询可能触发靓号懒过期,因此仍走 service 层而不是直接读 repository。 user, err := s.userSvc.GetUser(ctx, req.GetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.GetUserResponse{User: toProtoUser(user)}, nil } // BatchGetUsers 批量返回用户主状态,缺失用户不填充占位对象。 func (s *Server) BatchGetUsers(ctx context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // 批量查询缺失用户不填充空对象,客户端按 map key 判断存在。 users, err := s.userSvc.BatchGetUsers(ctx, req.GetUserIds()) if err != nil { return nil, xerr.ToGRPCError(err) } result := make(map[int64]*userv1.User, len(users)) for userID, user := range users { // proto map key 使用 user_id,value 是当前有效用户投影。 result[userID] = toProtoUser(user) } return &userv1.BatchGetUsersResponse{Users: result}, nil } // UpdateUserProfile 修改当前用户基础资料。 func (s *Server) UpdateUserProfile(ctx context.Context, req *userv1.UpdateUserProfileRequest) (*userv1.UpdateUserProfileResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // 可选字段由 proto optional 表达,service 层根据 nil 区分“不修改”和“清空”。 user, err := s.userSvc.UpdateUserProfile(ctx, req.GetUserId(), req.Username, req.Avatar, req.Gender, req.Birth) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.UpdateUserProfileResponse{User: toProtoUser(user)}, nil } // ChangeUserCountry 单独修改国家并返回下一次可修改时间。 func (s *Server) ChangeUserCountry(ctx context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // 国家修改有独立审计和冷却期,不能混进普通资料更新。 user, nextChangeAllowedAtMs, err := s.userSvc.ChangeUserCountry(ctx, req.GetUserId(), req.GetCountry(), req.GetMeta().GetRequestId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.ChangeUserCountryResponse{User: toProtoUser(user), NextChangeAllowedAtMs: nextChangeAllowedAtMs}, nil } // CompleteOnboarding 原子完成注册页必填资料。 func (s *Server) CompleteOnboarding(ctx context.Context, req *userv1.CompleteOnboardingRequest) (*userv1.CompleteOnboardingResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) if s.authSvc == nil { // 完成注册后必须重签 access token;缺少 auth service 时不能先写资料再让客户端继续拿旧 token。 return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "auth service is not configured")) } if !s.authSvc.TokenConfigValid() { // 签名配置缺失时提前失败,避免资料已完成但响应无法带新 access token。 return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "token service is not configured")) } // gateway 只透传已鉴权 user_id;资料校验和国家解析必须停留在 user-service。 user, err := s.userSvc.CompleteOnboarding(ctx, req.GetUserId(), req.GetUsername(), req.GetAvatar(), req.GetGender(), req.GetCountry()) if err != nil { return nil, xerr.ToGRPCError(err) } token, err := s.authSvc.IssueAccessTokenForSession(ctx, req.GetUserId(), req.GetMeta().GetSessionId(), authMeta(req.GetMeta())) if err != nil { // token 签发失败时返回错误,避免客户端拿旧 profile_required JWT 继续撞 profile gate。 return nil, xerr.ToGRPCError(err) } return &userv1.CompleteOnboardingResponse{ User: toProtoUser(user), ProfileCompleted: user.ProfileCompleted, ProfileCompletedAtMs: user.ProfileCompletedAtMs, OnboardingStatus: string(user.OnboardingStatus), Token: toProtoToken(token), }, nil } // ListCountries 返回国家主数据列表。 func (s *Server) ListCountries(ctx context.Context, req *userv1.ListCountriesRequest) (*userv1.ListCountriesResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) countries, err := s.userSvc.ListCountries(ctx, req.Enabled) if err != nil { return nil, xerr.ToGRPCError(err) } result := make([]*userv1.Country, 0, len(countries)) for _, country := range countries { result = append(result, toProtoCountry(country)) } return &userv1.ListCountriesResponse{Countries: result}, nil } // UpdateCountry 修改国家展示字段,country_code 不允许原地修改。 func (s *Server) UpdateCountry(ctx context.Context, req *userv1.UpdateCountryRequest) (*userv1.CountryResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) country, err := s.userSvc.UpdateCountry(ctx, userdomain.UpdateCountryCommand{ CountryID: req.GetCountryId(), CountryName: req.GetCountryName(), ISOAlpha3: req.GetIsoAlpha3(), ISONumeric: req.GetIsoNumeric(), CountryDisplayName: req.GetCountryDisplayName(), PhoneCountryCode: req.GetPhoneCountryCode(), Flag: req.GetFlag(), SortOrder: req.GetSortOrder(), OperatorUserID: req.GetOperatorUserId(), RequestID: req.GetMeta().GetRequestId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.CountryResponse{Country: toProtoCountry(country)}, nil } // ListRegistrationCountries 返回 App 注册页公开国家列表。 func (s *Server) ListRegistrationCountries(ctx context.Context, req *userv1.ListRegistrationCountriesRequest) (*userv1.ListRegistrationCountriesResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) countries, err := s.userSvc.ListRegistrationCountries(ctx) if err != nil { return nil, xerr.ToGRPCError(err) } result := make([]*userv1.Country, 0, len(countries)) for _, country := range countries { result = append(result, toProtoCountry(country)) } return &userv1.ListRegistrationCountriesResponse{Countries: result}, nil } // ListRegions 返回区域列表。 func (s *Server) ListRegions(ctx context.Context, req *userv1.ListRegionsRequest) (*userv1.ListRegionsResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) regions, err := s.userSvc.ListRegions(ctx, req.GetStatus()) if err != nil { return nil, xerr.ToGRPCError(err) } result := make([]*userv1.Region, 0, len(regions)) for _, region := range regions { result = append(result, toProtoRegion(region)) } return &userv1.ListRegionsResponse{Regions: result}, nil } // GetRegion 返回单个区域及其 active 国家列表。 func (s *Server) GetRegion(ctx context.Context, req *userv1.GetRegionRequest) (*userv1.RegionResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) region, err := s.userSvc.GetRegion(ctx, req.GetRegionId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.RegionResponse{Region: toProtoRegion(region)}, nil } // UpdateRegion 修改区域展示字段,不改变 countries。 func (s *Server) UpdateRegion(ctx context.Context, req *userv1.UpdateRegionRequest) (*userv1.RegionResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) region, err := s.userSvc.UpdateRegion(ctx, req.GetRegionId(), req.GetRegionCode(), req.GetName(), req.GetSortOrder(), req.GetOperatorUserId(), req.GetMeta().GetRequestId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.RegionResponse{Region: toProtoRegion(region)}, nil } // ReplaceRegionCountries 原子替换区域国家归属。 func (s *Server) ReplaceRegionCountries(ctx context.Context, req *userv1.ReplaceRegionCountriesRequest) (*userv1.RegionResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) region, err := s.userSvc.ReplaceRegionCountries(ctx, req.GetRegionId(), req.GetCountries(), req.GetOperatorUserId(), req.GetMeta().GetRequestId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.RegionResponse{Region: toProtoRegion(region)}, nil } // GetUserIdentity 按系统 user_id 查询当前 active display_user_id。 func (s *Server) GetUserIdentity(ctx context.Context, req *userv1.GetUserIdentityRequest) (*userv1.GetUserIdentityResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // Identity 查询返回当前 active display_user_id,过期靓号会在 service 层恢复。 identity, err := s.userSvc.GetUserIdentity(ctx, req.GetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.GetUserIdentityResponse{Identity: toProtoIdentity(identity)}, nil } // ResolveDisplayUserID 把展示短号解析为系统 user_id。 func (s *Server) ResolveDisplayUserID(ctx context.Context, req *userv1.ResolveDisplayUserIDRequest) (*userv1.ResolveDisplayUserIDResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // display_user_id 解析只接受当前 active 短号,不解析 held 默认号或过期靓号。 identity, err := s.userSvc.ResolveDisplayUserID(ctx, req.GetDisplayUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.ResolveDisplayUserIDResponse{Identity: toProtoIdentity(identity)}, nil } // ChangeDisplayUserID 修改当前 active 短号并返回新绑定。 func (s *Server) ChangeDisplayUserID(ctx context.Context, req *userv1.ChangeDisplayUserIDRequest) (*userv1.ChangeDisplayUserIDResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // 默认短号修改会检查占用和冷却期,具体事务由 service/repository 完成。 identity, err := s.userSvc.ChangeDisplayUserID(ctx, req.GetUserId(), req.GetNewDisplayUserId(), req.GetReason(), req.GetOperatorUserId(), req.GetMeta().GetRequestId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.ChangeDisplayUserIDResponse{Identity: toProtoIdentity(identity)}, nil } // ApplyPrettyDisplayUserID 申请临时靓号并覆盖当前展示号。 func (s *Server) ApplyPrettyDisplayUserID(ctx context.Context, req *userv1.ApplyPrettyDisplayUserIDRequest) (*userv1.ApplyPrettyDisplayUserIDResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // 靓号申请要求 payment_receipt_id,user-service 只保存回执不处理账务。 identity, leaseID, err := s.userSvc.ApplyPrettyDisplayUserID(ctx, req.GetUserId(), req.GetPrettyDisplayUserId(), req.GetLeaseDurationMs(), req.GetPaymentReceiptId(), req.GetMeta().GetRequestId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.ApplyPrettyDisplayUserIDResponse{Identity: toProtoIdentity(identity), LeaseId: leaseID}, nil } // ExpirePrettyDisplayUserID 主动触发靓号过期恢复。 func (s *Server) ExpirePrettyDisplayUserID(ctx context.Context, req *userv1.ExpirePrettyDisplayUserIDRequest) (*userv1.ExpirePrettyDisplayUserIDResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) // 过期恢复可由定时任务、懒过期或人工 RPC 触发,事务语义一致。 identity, err := s.userSvc.ExpirePrettyDisplayUserID(ctx, req.GetUserId(), req.GetLeaseId(), req.GetMeta().GetRequestId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.ExpirePrettyDisplayUserIDResponse{Identity: toProtoIdentity(identity)}, nil } func contextWithApp(ctx context.Context, meta *userv1.RequestMeta) context.Context { if meta == nil { return appcode.WithContext(ctx, "") } return appcode.WithContext(ctx, meta.GetAppCode()) }