2026-05-29 19:56:14 +08:00

747 lines
33 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 将 user-service 认证、用户和短号用例适配成 protobuf gRPC 服务。
package grpc
import (
"context"
"strings"
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"
mictimeservice "hyapp/services/user-service/internal/service/mictime"
userservice "hyapp/services/user-service/internal/service/user"
)
// Server 把 user-service 用例层适配为 protobuf gRPC 接口。
type Server struct {
// UnimplementedAuthServiceServer 提供未实现 RPC 的默认返回。
userv1.UnimplementedAuthServiceServer
// UnimplementedUserServiceServer 提供未实现 RPC 的默认返回。
userv1.UnimplementedUserServiceServer
// UnimplementedUserSocialServiceServer 提供访问、关注、好友 RPC 的默认返回。
userv1.UnimplementedUserSocialServiceServer
// UnimplementedUserDeviceServiceServer 提供设备推送 token RPC 的默认返回。
userv1.UnimplementedUserDeviceServiceServer
// 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
// UnimplementedUserCronServiceServer 提供 cron-service 批处理 RPC 的默认返回。
userv1.UnimplementedUserCronServiceServer
// authSvc 承载登录、设置密码、refresh 和 logout。
authSvc *authservice.Service
// userSvc 承载用户主数据和 display_user_id/靓号用例。
userSvc *userservice.Service
// hostSvc 承载 user-service 内 host/Agency/BD 事实用例。
hostSvc *hostservice.Service
// micTimeSvc 承载用户维度麦位在线时长聚合查询。
micTimeSvc *mictimeservice.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
}
// SetMicTimeService 挂载可选的麦位时长 read model避免破坏现有测试构造函数签名。
func (s *Server) SetMicTimeService(micTimeSvc *mictimeservice.Service) {
s.micTimeSvc = micTimeSvc
}
// ResolveApp 把客户端包名映射成内部 app_codegateway 后续会把 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
}
// QuickCreateAccount 为游戏联调快速创建一个已完成资料且可密码登录的账号。
func (s *Server) QuickCreateAccount(ctx context.Context, req *userv1.QuickCreateAccountRequest) (*userv1.QuickCreateAccountResponse, 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(),
}
token, err := s.authSvc.QuickCreateAccount(ctx, req.GetPassword(), registration, authMeta(req.GetMeta()))
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.QuickCreateAccountResponse{Token: toProtoToken(token), 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
}
// RecordLoginBlocked 记录 gateway 登录入口快拦审计,不参与认证主链路。
func (s *Server) RecordLoginBlocked(ctx context.Context, req *userv1.RecordLoginBlockedRequest) (*userv1.RecordLoginBlockedResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
recorded := s.authSvc.RecordLoginBlocked(ctx, authMeta(req.GetMeta()), req.GetChannel(), req.GetPlatform(), req.GetLanguage(), req.GetTimezone(), req.GetBlockReason())
return &userv1.RecordLoginBlockedResponse{Recorded: recorded}, 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
}
// BusinessUserLookup 是带业务场景和能力校验的用户查询入口。
func (s *Server) BusinessUserLookup(ctx context.Context, req *userv1.BusinessUserLookupRequest) (*userv1.BusinessUserLookupResponse, error) {
if s.hostSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
}
ctx = contextWithApp(ctx, req.GetMeta())
capability, ok := capabilityForBusinessLookupScene(req.GetScene())
if !ok {
return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "business lookup scene is invalid"))
}
allowed, reason, err := s.hostSvc.CheckBusinessCapability(ctx, req.GetActorUserId(), capability)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
if !allowed {
if reason == "" {
reason = "permission denied"
}
return nil, xerr.ToGRPCError(xerr.New(xerr.PermissionDenied, reason))
}
users, err := s.userSvc.BusinessUserLookup(ctx, req.GetScene(), req.GetKeyword(), req.GetPageSize())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &userv1.BusinessUserLookupResponse{Users: make([]*userv1.BusinessUserLookupItem, 0, len(users))}
for _, user := range users {
resp.Users = append(resp.Users, toProtoBusinessUserLookupItem(user))
}
return resp, nil
}
func capabilityForBusinessLookupScene(scene string) (string, bool) {
switch strings.ToLower(strings.TrimSpace(scene)) {
case userservice.BusinessSceneManagerResourceGrant:
return hostservice.CapabilityManagerResourceGrant, true
default:
return "", false
}
}
// GetMyProfileStats 返回我的页顶部统计计数。
func (s *Server) GetMyProfileStats(ctx context.Context, req *userv1.GetMyProfileStatsRequest) (*userv1.GetMyProfileStatsResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
stats, err := s.userSvc.GetMyProfileStats(ctx, req.GetUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.GetMyProfileStatsResponse{Stats: toProtoProfileStats(stats)}, nil
}
// SetUserStatus 处理平台封禁、停用和恢复用户。
func (s *Server) SetUserStatus(ctx context.Context, req *userv1.SetUserStatusRequest) (*userv1.SetUserStatusResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
result, err := s.userSvc.SetUserStatus(ctx, userservice.UserStatusCommand{
AppCode: req.GetMeta().GetAppCode(),
TargetUserID: req.GetTargetUserId(),
Status: fromProtoStatus(req.GetStatus()),
OperatorType: req.GetOperatorType(),
OperatorUserID: req.GetOperatorUserId(),
Reason: req.GetReason(),
RequestID: req.GetMeta().GetRequestId(),
NowMs: req.GetMeta().GetSentAtMs(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.SetUserStatusResponse{
User: toProtoUser(result.User),
RevokedSessionCount: result.RevokedSessionCount,
AccessTokenRevoked: result.AccessTokenRevoked,
AccessTokenRevokeError: result.AccessTokenError,
ImKicked: result.IMKicked,
ImKickError: result.IMKickError,
RoomEvicted: result.RoomEvicted,
RoomId: result.RoomID,
RtcKicked: result.RTCKicked,
RtcKickError: result.RTCKickError,
RoomEvictError: result.RoomEvictError,
}, nil
}
// RecordProfileVisit 记录访问足迹;重复访问只刷新访问次数和最后访问时间。
func (s *Server) RecordProfileVisit(ctx context.Context, req *userv1.RecordProfileVisitRequest) (*userv1.RecordProfileVisitResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
recorded, stats, err := s.userSvc.RecordProfileVisit(ctx, req.GetVisitorUserId(), req.GetTargetUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.RecordProfileVisitResponse{Recorded: recorded, TargetStats: toProtoProfileStats(stats)}, nil
}
// ListProfileVisitors 返回访问当前用户主页的人,按最后访问时间倒序。
func (s *Server) ListProfileVisitors(ctx context.Context, req *userv1.ListProfileVisitorsRequest) (*userv1.ListProfileVisitorsResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
records, total, err := s.userSvc.ListProfileVisitors(ctx, req.GetUserId(), req.GetPage(), req.GetPageSize())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &userv1.ListProfileVisitorsResponse{Records: make([]*userv1.ProfileVisitRecord, 0, len(records)), Total: total}
for _, record := range records {
resp.Records = append(resp.Records, toProtoProfileVisitRecord(record))
}
return resp, nil
}
// FollowUser 建立关注关系;重复关注保持幂等。
func (s *Server) FollowUser(ctx context.Context, req *userv1.FollowUserRequest) (*userv1.FollowUserResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
stats, err := s.userSvc.FollowUser(ctx, req.GetFollowerUserId(), req.GetFolloweeUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.FollowUserResponse{Following: true, FollowerStats: toProtoProfileStats(stats)}, nil
}
// UnfollowUser 取消关注关系;未关注时按幂等成功处理。
func (s *Server) UnfollowUser(ctx context.Context, req *userv1.UnfollowUserRequest) (*userv1.UnfollowUserResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
stats, err := s.userSvc.UnfollowUser(ctx, req.GetFollowerUserId(), req.GetFolloweeUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.UnfollowUserResponse{Following: false, FollowerStats: toProtoProfileStats(stats)}, nil
}
// ListFollowing 返回当前用户正在关注的人。
func (s *Server) ListFollowing(ctx context.Context, req *userv1.ListFollowingRequest) (*userv1.ListFollowingResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
records, total, err := s.userSvc.ListFollowing(ctx, req.GetUserId(), req.GetPage(), req.GetPageSize(), req.GetCursorUpdatedAtMs(), req.GetCursorUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &userv1.ListFollowingResponse{Records: make([]*userv1.FollowRecord, 0, len(records)), Total: total}
for _, record := range records {
resp.Records = append(resp.Records, toProtoFollowRecord(record))
}
return resp, nil
}
// ApplyFriend 创建好友申请。客户端随后用 IM 把申请消息送进聊天窗口。
func (s *Server) ApplyFriend(ctx context.Context, req *userv1.ApplyFriendRequest) (*userv1.ApplyFriendResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
application, alreadyFriends, err := s.userSvc.ApplyFriend(ctx, req.GetRequesterUserId(), req.GetTargetUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.ApplyFriendResponse{Application: toProtoFriendApplication(application), AlreadyFriends: alreadyFriends}, nil
}
// AcceptFriendApplication 在聊天窗口同意申请后,把申请转成双向好友关系。
func (s *Server) AcceptFriendApplication(ctx context.Context, req *userv1.AcceptFriendApplicationRequest) (*userv1.AcceptFriendApplicationResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
friend, err := s.userSvc.AcceptFriendApplication(ctx, req.GetAccepterUserId(), req.GetRequesterUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.AcceptFriendApplicationResponse{Friend: toProtoFriendRecord(friend)}, nil
}
// DeleteFriend 删除双向好友关系;缺失关系时按幂等成功返回 deleted=false。
func (s *Server) DeleteFriend(ctx context.Context, req *userv1.DeleteFriendRequest) (*userv1.DeleteFriendResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
deleted, err := s.userSvc.DeleteFriend(ctx, req.GetUserId(), req.GetFriendUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.DeleteFriendResponse{Deleted: deleted}, nil
}
// ListFriends 返回好友列表。
func (s *Server) ListFriends(ctx context.Context, req *userv1.ListFriendsRequest) (*userv1.ListFriendsResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
records, total, err := s.userSvc.ListFriends(ctx, req.GetUserId(), req.GetPage(), req.GetPageSize(), req.GetCursorUpdatedAtMs(), req.GetCursorUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &userv1.ListFriendsResponse{Records: make([]*userv1.FriendRecord, 0, len(records)), Total: total}
for _, record := range records {
resp.Records = append(resp.Records, toProtoFriendRecord(record))
}
return resp, nil
}
// ListFriendApplications 返回待处理好友申请direction 支持 incoming/outgoing。
func (s *Server) ListFriendApplications(ctx context.Context, req *userv1.ListFriendApplicationsRequest) (*userv1.ListFriendApplicationsResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
applications, total, err := s.userSvc.ListFriendApplications(ctx, req.GetUserId(), req.GetDirection(), req.GetPage(), req.GetPageSize())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &userv1.ListFriendApplicationsResponse{Applications: make([]*userv1.FriendApplication, 0, len(applications)), Total: total}
for _, application := range applications {
resp.Applications = append(resp.Applications, toProtoFriendApplication(application))
}
return resp, nil
}
// SubmitReport 记录 App 用户提交的用户或房间举报。
func (s *Server) SubmitReport(ctx context.Context, req *userv1.SubmitReportRequest) (*userv1.SubmitReportResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
report, err := s.userSvc.SubmitReport(ctx, req.GetReporterUserId(), req.GetUserId(), req.GetRoomId(), req.GetReportType(), req.GetReason(), req.GetImageUrls(), req.GetMeta().GetRequestId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.SubmitReportResponse{Report: toProtoUserReport(report)}, 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_idvalue 是当前有效用户投影。
result[userID] = toProtoUser(user)
}
return &userv1.BatchGetUsersResponse{Users: result}, nil
}
// ListUserIDs 为后台 fanout 任务返回稳定递增的用户 ID 游标页。
func (s *Server) ListUserIDs(ctx context.Context, req *userv1.ListUserIDsRequest) (*userv1.ListUserIDsResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
userIDs, nextCursor, done, err := s.userSvc.ListUserIDs(ctx, userdomain.UserIDPageFilter{
TargetScope: req.GetTargetScope(),
CursorUserID: req.GetCursorUserId(),
PageSize: int(req.GetPageSize()),
RegionID: req.GetRegionId(),
Country: req.GetCountry(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.ListUserIDsResponse{UserIds: userIDs, NextCursorUserId: nextCursor, Done: done}, nil
}
// GetUserMicLifetimeStats 返回用户维度全部历史麦位时长,主播任务后续也读这份基础指标。
func (s *Server) GetUserMicLifetimeStats(ctx context.Context, req *userv1.GetUserMicLifetimeStatsRequest) (*userv1.GetUserMicLifetimeStatsResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
if s.micTimeSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "mic time service is not configured"))
}
appCode := ""
if req.GetMeta() != nil {
appCode = req.GetMeta().GetAppCode()
}
stats, err := s.micTimeSvc.GetLifetimeStats(ctx, appCode, req.GetUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.GetUserMicLifetimeStatsResponse{Stats: toProtoMicLifetimeStats(stats)}, 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())
// 国家修改有独立审计和冷却期,不能混进普通资料更新。
before, err := s.userSvc.GetUser(ctx, req.GetUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
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,
OldRegionId: before.RegionID,
NewRegionId: user.RegionID,
RegionChanged: before.RegionID != user.RegionID,
}, 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(), req.GetInviteCode(), req.GetMeta().GetRequestId())
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),
Invite: toProtoInviteBinding(user.InviteBinding),
}, nil
}
// BindPushToken 绑定当前用户设备推送 token。
func (s *Server) BindPushToken(ctx context.Context, req *userv1.BindPushTokenRequest) (*userv1.BindPushTokenResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
updatedAtMS, err := s.userSvc.BindPushToken(ctx, req.GetUserId(), req.GetDeviceId(), req.GetPushToken(), req.GetProvider(), req.GetPlatform(), req.GetAppVersion(), req.GetLanguage(), req.GetTimezone(), req.GetMeta().GetRequestId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.BindPushTokenResponse{Bound: true, UpdatedAtMs: updatedAtMS}, nil
}
// DeletePushToken 失效当前用户设备推送 token。
func (s *Server) DeletePushToken(ctx context.Context, req *userv1.DeletePushTokenRequest) (*userv1.DeletePushTokenResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
deleted, updatedAtMS, err := s.userSvc.DeletePushToken(ctx, req.GetUserId(), req.GetDeviceId(), req.GetPushToken(), req.GetMeta().GetRequestId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.DeletePushTokenResponse{Deleted: deleted, UpdatedAtMs: updatedAtMS}, 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
}
// ListLoginRiskBlockedCountries 返回 App 登录后异步风控可消费的地区屏蔽词表。
func (s *Server) ListLoginRiskBlockedCountries(ctx context.Context, req *userv1.ListLoginRiskBlockedCountriesRequest) (*userv1.ListLoginRiskBlockedCountriesResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
blocks, err := s.authSvc.ListLoginRiskBlockedCountries(ctx)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
result := make([]*userv1.LoginRiskBlockedCountry, 0, len(blocks))
var updatedAtMs int64
for _, block := range blocks {
if block.UpdatedAtMs > updatedAtMs {
updatedAtMs = block.UpdatedAtMs
}
result = append(result, &userv1.LoginRiskBlockedCountry{
CountryCode: block.CountryCode,
Keyword: block.Keyword,
UpdatedAtMs: block.UpdatedAtMs,
})
}
return &userv1.ListLoginRiskBlockedCountriesResponse{Countries: result, UpdatedAtMs: updatedAtMs}, 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_iduser-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())
}