2026-04-29 12:43:05 +08:00

340 lines
15 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/api/proto/user/v1"
"hyapp/pkg/xerr"
authdomain "hyapp/services/user-service/internal/domain/auth"
authservice "hyapp/services/user-service/internal/service/auth"
userservice "hyapp/services/user-service/internal/service/user"
)
// Server 把 user-service 用例层适配为 protobuf gRPC 接口。
type Server struct {
// UnimplementedAuthServiceServer 保持新增 RPC 时的向前兼容默认行为。
userv1.UnimplementedAuthServiceServer
// UnimplementedUserServiceServer 保持用户服务新增 RPC 时的向前兼容默认行为。
userv1.UnimplementedUserServiceServer
// UnimplementedUserIdentityServiceServer 保持短号服务新增 RPC 时的向前兼容默认行为。
userv1.UnimplementedUserIdentityServiceServer
// UnimplementedCountryAdminServiceServer 保持国家管理 RPC 新增时的默认兼容行为。
userv1.UnimplementedCountryAdminServiceServer
// UnimplementedRegionAdminServiceServer 保持区域管理 RPC 新增时的默认兼容行为。
userv1.UnimplementedRegionAdminServiceServer
// authSvc 承载登录、设置密码、refresh 和 logout。
authSvc *authservice.Service
// userSvc 承载用户主数据和 display_user_id/靓号用例。
userSvc *userservice.Service
}
// NewServer 创建 user-service gRPC server。
func NewServer(authSvc *authservice.Service, userSvc *userservice.Service) *Server {
// gRPC 层只做协议转换和错误转换,不保存领域状态。
return &Server{
authSvc: authSvc,
userSvc: userSvc,
}
}
// LoginPassword 统一把短号不存在、未设置密码和密码错误留给 service 层映射为 AUTH_FAILED。
func (s *Server) LoginPassword(ctx context.Context, req *userv1.LoginPasswordRequest) (*userv1.AuthResponse, error) {
// 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)}, nil
}
// LoginThirdParty 只接受 provider 凭证,不在 gateway 泄漏 provider secret。
func (s *Server) LoginThirdParty(ctx context.Context, req *userv1.LoginThirdPartyRequest) (*userv1.AuthResponse, error) {
registration := authdomain.ThirdPartyRegistration{
Username: req.GetUsername(),
Gender: req.GetGender(),
Country: req.GetCountry(),
InviteCode: req.GetInviteCode(),
IP: req.GetMeta().GetClientIp(),
DeviceID: firstNonEmpty(req.GetDeviceId(), req.GetMeta().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}, nil
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
// gRPC request fields win over meta; meta is only a gateway fallback.
return trimmed
}
}
return ""
}
// SetPassword 为已登录用户首次写入密码身份。
func (s *Server) SetPassword(ctx context.Context, req *userv1.SetPasswordRequest) (*userv1.SetPasswordResponse, error) {
// 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) {
// 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) {
// 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) {
// 用户查询可能触发靓号懒过期,因此仍走 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) {
// 批量查询缺失用户不填充空对象,客户端按 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
}
// UpdateUserProfile 修改当前用户基础资料。
func (s *Server) UpdateUserProfile(ctx context.Context, req *userv1.UpdateUserProfileRequest) (*userv1.UpdateUserProfileResponse, error) {
// 可选字段由 proto optional 表达service 层根据 nil 区分“不修改”和“清空”。
user, err := s.userSvc.UpdateUserProfile(ctx, req.GetUserId(), req.Username, req.Avatar, 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) {
// 国家修改有独立审计和冷却期,不能混进普通资料更新。
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
}
// CreateCountry 创建国家主数据;公网 admin API 暴露前必须由 gateway 做管理鉴权。
func (s *Server) CreateCountry(ctx context.Context, req *userv1.CreateCountryRequest) (*userv1.CountryResponse, error) {
country, err := s.userSvc.CreateCountry(ctx, req.GetCountryName(), req.GetCountryCode(), req.GetCountryDisplayName(), req.GetSortOrder(), req.GetOperatorUserId(), req.GetMeta().GetRequestId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.CountryResponse{Country: toProtoCountry(country)}, nil
}
// ListCountries 返回国家主数据列表。
func (s *Server) ListCountries(ctx context.Context, req *userv1.ListCountriesRequest) (*userv1.ListCountriesResponse, error) {
countries, err := s.userSvc.ListCountries(ctx, req.GetStatus())
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) {
country, err := s.userSvc.UpdateCountry(ctx, req.GetCountryId(), req.GetCountryName(), req.GetCountryDisplayName(), req.GetSortOrder(), req.GetOperatorUserId(), req.GetMeta().GetRequestId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.CountryResponse{Country: toProtoCountry(country)}, nil
}
// DisableCountry 停用国家,后续注册、改国家和区域配置都不能再选它。
func (s *Server) DisableCountry(ctx context.Context, req *userv1.DisableCountryRequest) (*userv1.CountryResponse, error) {
country, err := s.userSvc.DisableCountry(ctx, req.GetCountryId(), req.GetOperatorUserId(), req.GetMeta().GetRequestId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.CountryResponse{Country: toProtoCountry(country)}, nil
}
// CreateRegion 创建区域并配置 active 国家列表。
func (s *Server) CreateRegion(ctx context.Context, req *userv1.CreateRegionRequest) (*userv1.RegionResponse, error) {
region, err := s.userSvc.CreateRegion(ctx, req.GetRegionCode(), req.GetName(), req.GetCountries(), req.GetSortOrder(), req.GetOperatorUserId(), req.GetMeta().GetRequestId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.RegionResponse{Region: toProtoRegion(region)}, nil
}
// ListRegions 返回区域列表。
func (s *Server) ListRegions(ctx context.Context, req *userv1.ListRegionsRequest) (*userv1.ListRegionsResponse, error) {
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) {
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) {
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) {
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
}
// DisableRegion 停用区域并释放 active 国家归属。
func (s *Server) DisableRegion(ctx context.Context, req *userv1.DisableRegionRequest) (*userv1.RegionResponse, error) {
region, err := s.userSvc.DisableRegion(ctx, req.GetRegionId(), 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) {
// 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) {
// 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) {
// 默认短号修改会检查占用和冷却期,具体事务由 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) {
// 靓号申请要求 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) {
// 过期恢复可由定时任务、懒过期或人工 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
}