package userapi import ( "log/slog" "net/http" "strconv" "strings" activityv1 "hyapp.local/api/proto/activity/v1" userv1 "hyapp.local/api/proto/user/v1" "hyapp/pkg/logx" "hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/transport/http/httpkit" ) // userIdentityData 是 gateway 对外返回的当前短号投影。 // `display_user_id` 永远表示当前有效展示号,临时靓号过期后由 user-service 恢复为默认短号。 type userIdentityData struct { UserID string `json:"user_id"` DisplayUserID string `json:"display_user_id"` PrettyID string `json:"pretty_id,omitempty"` PrettyDisplayUserID string `json:"pretty_display_user_id,omitempty"` Status string `json:"status"` DefaultDisplayUserID string `json:"default_display_user_id"` DisplayUserIDKind string `json:"display_user_id_kind"` DisplayUserIDExpiresAtMs int64 `json:"display_user_id_expires_at_ms"` LeaseID string `json:"lease_id,omitempty"` } type resolveDisplayUserData struct { userIdentityData Identity userIdentityData `json:"identity"` Profile userProfileData `json:"profile,omitempty"` } // userProfileData 是 gateway 对外返回的用户基础资料投影。 // 它不包含注册来源、三方身份或 refresh session 等服务端内部字段。 type userProfileData struct { UserID string `json:"user_id"` DisplayUserID string `json:"display_user_id"` PrettyID string `json:"pretty_id,omitempty"` PrettyDisplayUserID string `json:"pretty_display_user_id,omitempty"` DefaultDisplayUserID string `json:"default_display_user_id"` DisplayUserIDKind string `json:"display_user_id_kind"` DisplayUserIDExpiresAtMs int64 `json:"display_user_id_expires_at_ms"` Username string `json:"username"` Gender string `json:"gender,omitempty"` Country string `json:"country,omitempty"` CountryID int64 `json:"country_id,omitempty"` CountryName string `json:"country_name,omitempty"` CountryDisplayName string `json:"country_display_name,omitempty"` ISONumeric string `json:"iso_numeric,omitempty"` PhoneCountryCode string `json:"phone_country_code,omitempty"` CountryEnabled bool `json:"country_enabled"` RegionID int64 `json:"region_id,omitempty"` RegionCode string `json:"region_code,omitempty"` RegionName string `json:"region_name,omitempty"` Avatar string `json:"avatar,omitempty"` Birth string `json:"birth,omitempty"` Language string `json:"language,omitempty"` ProfileCompleted bool `json:"profile_completed"` ProfileCompletedAtMs int64 `json:"profile_completed_at_ms"` OnboardingStatus string `json:"onboarding_status"` CreatedAtMs int64 `json:"created_at_ms"` UpdatedAtMs int64 `json:"updated_at_ms"` ProfileBgImg string `json:"profile_bg_img"` ContactInfo string `json:"contact_info,omitempty"` NextCountryChangeAtMs int64 `json:"next_country_change_allowed_at_ms,omitempty"` } type prettyDisplayIDData struct { PrettyID string `json:"pretty_id"` DisplayUserID string `json:"display_user_id"` LevelTrack string `json:"level_track"` MinLevel int32 `json:"min_level"` MaxLevel int32 `json:"max_level"` } // completeOnboardingData 在保留资料字段顶层兼容性的同时返回新的 access token。 // token.refresh_token 首版为空,客户端继续持有登录时的 refresh token,仅替换 access_token。 type completeOnboardingData struct { userProfileData Invite *inviteBindingData `json:"invite,omitempty"` Token authTokenData `json:"token"` } type inviteBindingData struct { Bound bool `json:"bound"` InviteCode string `json:"invite_code,omitempty"` InviterUserID string `json:"inviter_user_id,omitempty"` } type inviteOverviewData struct { MyInviteCode string `json:"my_invite_code"` InviteEnabled bool `json:"invite_enabled"` InviteCount int64 `json:"invite_count"` ValidInviteCount int64 `json:"valid_invite_count"` ValidInviteThresholdCoin int64 `json:"valid_invite_threshold_coin"` } // countryData 是注册页国家列表的公开响应投影。 // 只包含客户端选择和展示所需字段,不暴露管理端审计字段。 type countryData struct { CountryID int64 `json:"country_id"` CountryName string `json:"country_name"` CountryCode string `json:"country_code"` ISOAlpha3 string `json:"iso_alpha3,omitempty"` ISONumeric string `json:"iso_numeric,omitempty"` CountryDisplayName string `json:"country_display_name"` PhoneCountryCode string `json:"phone_country_code,omitempty"` Enabled bool `json:"enabled"` Flag string `json:"flag,omitempty"` SortOrder int32 `json:"sort_order"` } // loginRiskBlockedCountryData 是 App 登录后异步地区风控只读词表。 // keyword 保留后台录入词,country_code 为空时表示该词只供客户端本地匹配。 type loginRiskBlockedCountryData struct { CountryCode string `json:"country_code,omitempty"` Keyword string `json:"keyword"` UpdatedAtMs int64 `json:"updated_at_ms"` } // listRegistrationCountries 返回 App 注册页可选国家。 // 该接口公开读,不要求 access token;user-service 负责只返回 active 且 enabled 的国家。 func (h *Handler) listRegistrationCountries(writer http.ResponseWriter, request *http.Request) { if h.userCountryClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.userCountryClient.ListRegistrationCountries(request.Context(), &userv1.ListRegistrationCountriesRequest{ Meta: httpkit.UserMeta(request, ""), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } countries := make([]countryData, 0, len(resp.GetCountries())) for _, country := range resp.GetCountries() { countries = append(countries, registrationCountryData(country)) } httpkit.WriteOK(writer, request, map[string]any{"countries": countries}) } // listLoginRiskBlockedCountries 返回 App 可异步拉取的地区屏蔽词表。 // 登录最终处置仍由 user-service 风控 worker 决定,客户端只做补充风险探测和上报。 func (h *Handler) listLoginRiskBlockedCountries(writer http.ResponseWriter, request *http.Request) { if h.userCountryClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.userCountryClient.ListLoginRiskBlockedCountries(request.Context(), &userv1.ListLoginRiskBlockedCountriesRequest{ Meta: httpkit.UserMeta(request, ""), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } countries := make([]loginRiskBlockedCountryData, 0, len(resp.GetCountries())) for _, country := range resp.GetCountries() { countries = append(countries, loginRiskBlockedCountryData{ CountryCode: country.GetCountryCode(), Keyword: country.GetKeyword(), UpdatedAtMs: country.GetUpdatedAtMs(), }) } httpkit.WriteOK(writer, request, map[string]any{"countries": countries, "updated_at_ms": resp.GetUpdatedAtMs()}) } // getMyIdentity 返回当前登录用户的短号状态。 func (h *Handler) getMyIdentity(writer http.ResponseWriter, request *http.Request) { if h.userIdentityClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.userIdentityClient.GetUserIdentity(request.Context(), &userv1.GetUserIdentityRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } identity := resp.GetIdentity() identityView := identityData(identity, "") data := resolveDisplayUserData{ userIdentityData: identityView, Identity: identityView, } if h.userProfileClient != nil && identity.GetUserId() > 0 { profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{ Meta: httpkit.UserMeta(request, ""), UserId: identity.GetUserId(), }) if err != nil { logx.Warn(request.Context(), "resolve_display_user_profile_failed", slog.Int64("user_id", identity.GetUserId()), slog.String("error", err.Error())) } else { data.Profile = profileData(profileResp.GetUser(), 0) } } httpkit.WriteOK(writer, request, data) } // resolveDisplayUserID 通过当前有效展示号解析用户。 func (h *Handler) resolveDisplayUserID(writer http.ResponseWriter, request *http.Request) { if h.userIdentityClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } displayUserID := strings.TrimSpace(request.PathValue("display_user_id")) if displayUserID == "" { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.userIdentityClient.ResolveDisplayUserID(request.Context(), &userv1.ResolveDisplayUserIDRequest{ Meta: httpkit.UserMeta(request, ""), DisplayUserId: displayUserID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } identity := resp.GetIdentity() identityView := identityData(identity, "") data := resolveDisplayUserData{ userIdentityData: identityView, Identity: identityView, } if h.userProfileClient != nil && identity.GetUserId() > 0 { // H5 搜索短号后要展示头像和昵称;短号解析只负责稳定 ID,资料展示必须回 user-service 用 user_id 再补齐。 profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{ Meta: httpkit.UserMeta(request, ""), UserId: identity.GetUserId(), }) if err != nil { logx.Warn(request.Context(), "resolve_display_user_profile_failed", slog.Int64("user_id", identity.GetUserId()), slog.String("error", err.Error())) } else { data.Profile = profileData(profileResp.GetUser(), 0) } } httpkit.WriteOK(writer, request, data) } // completeMyOnboarding 是注册页唯一资料提交入口。 // gateway 只负责从 access token 取 user_id,资料校验和国家/区域解析必须由 user-service 同事务完成。 func (h *Handler) completeMyOnboarding(writer http.ResponseWriter, request *http.Request) { var body struct { Username string `json:"username"` Avatar string `json:"avatar"` Gender string `json:"gender"` Country string `json:"country"` InviteCode string `json:"invite_code"` } if !httpkit.Decode(writer, request, &body) { return } if h.userProfileClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.userProfileClient.CompleteOnboarding(request.Context(), &userv1.CompleteOnboardingRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), Username: body.Username, Avatar: body.Avatar, Gender: body.Gender, Country: body.Country, InviteCode: body.InviteCode, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, completeOnboardingData{ userProfileData: profileData(resp.GetUser(), 0), Invite: inviteBindingDataFromProto(resp.GetInvite()), Token: authData(resp.GetToken(), nil), }) } // getMyProfile 返回当前登录用户的基础资料。 func (h *Handler) getMyProfile(writer http.ResponseWriter, request *http.Request) { if h.userProfileClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, profileData(resp.GetUser(), 0)) } func (h *Handler) getUserProfile(writer http.ResponseWriter, request *http.Request) { if h.userProfileClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } userID, ok := httpkit.PositiveInt64PathValue(request, "user_id") if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } // 成就页的 user_id 模式是只读展示目标用户资料,不能回退到 token 用户,否则分享页会出现资料和成就归属不一致。 resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{ Meta: httpkit.UserMeta(request, ""), UserId: userID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, profileData(resp.GetUser(), 0)) } // getMyInviteOverview 返回当前登录用户自己的邀请码和邀请计数。 func (h *Handler) getMyInviteOverview(writer http.ResponseWriter, request *http.Request) { if h.userProfileClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, inviteOverviewDataFromProto(resp.GetUser().GetInvite())) } // updateMyProfile 修改当前用户的用户名、头像、性别和生日;主页背景图走专用上传接口。 func (h *Handler) updateMyProfile(writer http.ResponseWriter, request *http.Request) { var body struct { Username *string `json:"username"` Avatar *string `json:"avatar"` Gender *string `json:"gender"` Birth *string `json:"birth"` } if !httpkit.Decode(writer, request, &body) { return } if h.userProfileClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.userProfileClient.UpdateUserProfile(request.Context(), &userv1.UpdateUserProfileRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), Username: body.Username, Avatar: body.Avatar, Gender: body.Gender, Birth: body.Birth, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, profileData(resp.GetUser(), 0)) } // updateMyProfileBackground 修改当前用户个人信息页背景图;客户端先上传图片,本接口只保存 URL。 func (h *Handler) updateMyProfileBackground(writer http.ResponseWriter, request *http.Request) { var body struct { URL string `json:"url"` } if !httpkit.Decode(writer, request, &body) { return } if h.userProfileClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.userProfileClient.UpdateUserProfileBackground(request.Context(), &userv1.UpdateUserProfileBackgroundRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), ProfileBgImg: body.URL, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{ "profile_bg_img": resp.GetUser().GetProfileBgImg(), "profile": profileData(resp.GetUser(), 0), }) } type contactInfoUpdateBody struct { ContactInfo *string `json:"contact_info"` ContactInfoCamel *string `json:"contactInfo"` WhatsappPhone *string `json:"whatsapp_phone"` WhatsappPhoneCamel *string `json:"whatsappPhone"` Contact *string `json:"contact"` } // updateMyContactInfo 修改当前登录用户的用户级联系方式;多个经营身份页面共用这一个字段。 func (h *Handler) updateMyContactInfo(writer http.ResponseWriter, request *http.Request) { var body contactInfoUpdateBody if !httpkit.Decode(writer, request, &body) { return } if h.userProfileClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } contactInfo, ok := contactInfoFromBody(body) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "contact_info is required") return } resp, err := h.userProfileClient.UpdateUserContactInfo(request.Context(), &userv1.UpdateUserContactInfoRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), ContactInfo: contactInfo, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } // 兼容 H5 WhatsApp 字段命名,同时返回 profile,方便页面刷新本地用户资料。 httpkit.WriteOK(writer, request, map[string]any{ "contact_info": resp.GetUser().GetContactInfo(), "whatsapp_phone": resp.GetUser().GetContactInfo(), "profile": profileData(resp.GetUser(), 0), }) } func contactInfoFromBody(body contactInfoUpdateBody) (string, bool) { // 按字段是否存在判断用户是否提交,允许空字符串清空联系方式。 for _, value := range []*string{body.ContactInfo, body.ContactInfoCamel, body.WhatsappPhone, body.WhatsappPhoneCamel, body.Contact} { if value != nil { return strings.TrimSpace(*value), true } } return "", false } // changeMyCountry 单独修改当前用户国家;冷却期和审计由 user-service 保证。 func (h *Handler) changeMyCountry(writer http.ResponseWriter, request *http.Request) { var body struct { Country string `json:"country"` } if !httpkit.Decode(writer, request, &body) { return } if h.userProfileClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.userProfileClient.ChangeUserCountry(request.Context(), &userv1.ChangeUserCountryRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), Country: body.Country, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } h.removeOldRegionBroadcastMemberBestEffort(request, auth.UserIDFromContext(request.Context()), resp.GetOldRegionId(), resp.GetNewRegionId(), "user_country_changed") httpkit.WriteOK(writer, request, profileData(resp.GetUser(), resp.GetNextChangeAllowedAtMs())) } func (h *Handler) removeOldRegionBroadcastMemberBestEffort(request *http.Request, userID int64, oldRegionID int64, newRegionID int64, reason string) { if h == nil || h.broadcastClient == nil || userID <= 0 || oldRegionID <= 0 || oldRegionID == newRegionID { return } resp, err := h.broadcastClient.RemoveRegionBroadcastMember(request.Context(), &activityv1.RemoveRegionBroadcastMemberRequest{ Meta: httpkit.ActivityMeta(request), UserId: userID, RegionId: oldRegionID, Reason: reason, }) if err != nil { // 国家修改已经由 user-service 提交,IM 旧群成员移除是外部副作用;失败记录日志,后续 callback 仍会阻止用户重新加入旧区域群。 logx.Warn(request.Context(), "im_region_group_member_remove_failed", slog.Int64("user_id", userID), slog.Int64("old_region_id", oldRegionID), slog.Int64("new_region_id", newRegionID), slog.String("reason", reason), slog.String("error", err.Error()), ) return } logx.Info(request.Context(), "im_region_group_member_removed", slog.Int64("user_id", userID), slog.Int64("old_region_id", oldRegionID), slog.Int64("new_region_id", newRegionID), slog.String("group_id", resp.GetGroupId()), slog.Bool("removed", resp.GetRemoved()), ) } // changeMyDisplayUserID 修改当前登录用户的默认短号。 func (h *Handler) changeMyDisplayUserID(writer http.ResponseWriter, request *http.Request) { var body struct { NewDisplayUserID string `json:"new_display_user_id"` Reason string `json:"reason"` } if !httpkit.Decode(writer, request, &body) { return } if h.userIdentityClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.userIdentityClient.ChangeDisplayUserID(request.Context(), &userv1.ChangeDisplayUserIDRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), NewDisplayUserId: body.NewDisplayUserID, Reason: body.Reason, OperatorUserId: auth.UserIDFromContext(request.Context()), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, identityData(resp.GetIdentity(), "")) } // applyMyPrettyDisplayUserID 为当前登录用户申请临时靓号。 func (h *Handler) applyMyPrettyDisplayUserID(writer http.ResponseWriter, request *http.Request) { var body struct { PrettyDisplayUserID string `json:"pretty_display_user_id"` LeaseDurationMs int64 `json:"lease_duration_ms"` PaymentReceiptID string `json:"payment_receipt_id"` } if !httpkit.Decode(writer, request, &body) { return } if h.userIdentityClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.userIdentityClient.ApplyPrettyDisplayUserID(request.Context(), &userv1.ApplyPrettyDisplayUserIDRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), PrettyDisplayUserId: body.PrettyDisplayUserID, LeaseDurationMs: body.LeaseDurationMs, PaymentReceiptId: body.PaymentReceiptID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, identityData(resp.GetIdentity(), resp.GetLeaseId())) } // listAvailablePrettyDisplayIDs 返回当前登录用户等级可申请的靓号池号码。 func (h *Handler) listAvailablePrettyDisplayIDs(writer http.ResponseWriter, request *http.Request) { if h.userIdentityClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } page, ok := httpkit.PositiveInt32Query(request, "page", 1) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } // App 只能传分页参数;用户等级、池状态和号码可用性全部由 user-service 按当前登录用户重新计算。 resp, err := h.userIdentityClient.ListAvailablePrettyDisplayIDs(request.Context(), &userv1.ListAvailablePrettyDisplayIDsRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), Page: page, PageSize: pageSize, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } items := make([]prettyDisplayIDData, 0, len(resp.GetItems())) for _, item := range resp.GetItems() { // 这里返回申请所需 pretty_id 和展示用 display_user_id,等级区间只用于客户端展示申请门槛。 items = append(items, prettyDisplayIDData{ PrettyID: item.GetPrettyId(), DisplayUserID: item.GetDisplayUserId(), LevelTrack: item.GetPool().GetLevelTrack(), MinLevel: item.GetPool().GetMinLevel(), MaxLevel: item.GetPool().GetMaxLevel(), }) } httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize}) } // applyPrettyDisplayIDFromPool 按 pretty_id 申请靓号池号码,租期由 user-service 固定为长期。 func (h *Handler) applyPrettyDisplayIDFromPool(writer http.ResponseWriter, request *http.Request) { var body struct { PrettyID string `json:"pretty_id"` } if !httpkit.Decode(writer, request, &body) { return } if h.userIdentityClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } // 新靓号池申请只接收 pretty_id,不接受客户端传靓号内容或租期,防止绕过池状态和等级区间。 resp, err := h.userIdentityClient.ApplyPrettyDisplayIDFromPool(request.Context(), &userv1.ApplyPrettyDisplayIDFromPoolRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), PrettyId: strings.TrimSpace(body.PrettyID), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, identityData(resp.GetIdentity(), resp.GetLeaseId())) } func identityData(identity *userv1.UserIdentity, leaseID string) userIdentityData { if identity == nil { return userIdentityData{LeaseID: leaseID} } // identity 投影必须带上 pretty_id 和 pretty_display_user_id,App 可区分真实用户 ID、默认短号和当前靓号。 return userIdentityData{ UserID: userIDStringFromIdentity(identity), DisplayUserID: identity.GetDisplayUserId(), PrettyID: identity.GetPrettyId(), PrettyDisplayUserID: identity.GetPrettyDisplayUserId(), Status: identity.GetStatus(), DefaultDisplayUserID: identity.GetDefaultDisplayUserId(), DisplayUserIDKind: identity.GetDisplayUserIdKind(), DisplayUserIDExpiresAtMs: identity.GetDisplayUserIdExpiresAtMs(), LeaseID: leaseID, } } func profileData(user *userv1.User, nextCountryChangeAtMs int64) userProfileData { if user == nil { return userProfileData{NextCountryChangeAtMs: nextCountryChangeAtMs} } // 所有资料卡共用该投影,pretty 字段在个人信息、房间资料卡等调用点保持同名返回。 return userProfileData{ UserID: userIDStringFromUser(user), DisplayUserID: user.GetDisplayUserId(), PrettyID: user.GetPrettyId(), PrettyDisplayUserID: user.GetPrettyDisplayUserId(), DefaultDisplayUserID: user.GetDefaultDisplayUserId(), DisplayUserIDKind: user.GetDisplayUserIdKind(), DisplayUserIDExpiresAtMs: user.GetDisplayUserIdExpiresAtMs(), Username: user.GetUsername(), Gender: user.GetGender(), Country: user.GetCountry(), CountryID: user.GetCountryId(), CountryName: user.GetCountryName(), CountryDisplayName: user.GetCountryDisplayName(), ISONumeric: user.GetIsoNumeric(), PhoneCountryCode: user.GetPhoneCountryCode(), CountryEnabled: user.GetCountryEnabled(), RegionID: user.GetRegionId(), RegionCode: user.GetRegionCode(), RegionName: user.GetRegionName(), Avatar: user.GetAvatar(), Birth: user.GetBirth(), Language: user.GetLanguage(), ProfileCompleted: user.GetProfileCompleted(), ProfileCompletedAtMs: user.GetProfileCompletedAtMs(), OnboardingStatus: user.GetOnboardingStatus(), CreatedAtMs: user.GetCreatedAtMs(), UpdatedAtMs: user.GetUpdatedAtMs(), ProfileBgImg: user.GetProfileBgImg(), ContactInfo: user.GetContactInfo(), NextCountryChangeAtMs: nextCountryChangeAtMs, } } func inviteBindingDataFromProto(binding *userv1.InviteBinding) *inviteBindingData { if binding == nil { return nil } return &inviteBindingData{ Bound: binding.GetBound(), InviteCode: binding.GetInviteCode(), InviterUserID: userIDString(binding.GetInviterUserId()), } } func inviteOverviewDataFromProto(overview *userv1.InviteOverview) inviteOverviewData { if overview == nil { return inviteOverviewData{} } return inviteOverviewData{ MyInviteCode: overview.GetMyInviteCode(), InviteEnabled: overview.GetInviteEnabled(), InviteCount: overview.GetInviteCount(), ValidInviteCount: overview.GetValidInviteCount(), ValidInviteThresholdCoin: overview.GetValidInviteThresholdCoin(), } } func userIDString(userID int64) string { if userID > 0 { return strconv.FormatInt(userID, 10) } return "" } func userIDStringFromUser(user *userv1.User) string { if user == nil { return "" } return userIDString(user.GetUserId()) } func userIDStringFromIdentity(identity *userv1.UserIdentity) string { if identity == nil { return "" } return userIDString(identity.GetUserId()) } func registrationCountryData(country *userv1.Country) countryData { if country == nil { return countryData{} } return countryData{ CountryID: country.GetCountryId(), CountryName: country.GetCountryName(), CountryCode: country.GetCountryCode(), ISOAlpha3: country.GetIsoAlpha3(), ISONumeric: country.GetIsoNumeric(), CountryDisplayName: country.GetCountryDisplayName(), PhoneCountryCode: country.GetPhoneCountryCode(), Enabled: country.GetEnabled(), Flag: country.GetFlag(), SortOrder: country.GetSortOrder(), } }