2026-06-30 20:50:50 +08:00

770 lines
29 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 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 tokenuser-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_idApp 可区分真实用户 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(),
}
}