diff --git a/services/gateway-service/internal/client/user_client.go b/services/gateway-service/internal/client/user_client.go index be0318bd..00527ffc 100644 --- a/services/gateway-service/internal/client/user_client.go +++ b/services/gateway-service/internal/client/user_client.go @@ -52,6 +52,7 @@ type UserProfileClient interface { // UserInviteClient 抽象 H5 邀请页对 user-service 邀请人搜索和绑定能力的依赖。 type UserInviteClient interface { + GetInviteAttribution(ctx context.Context, req *userv1.GetInviteAttributionRequest) (*userv1.GetInviteAttributionResponse, error) SearchInviteReferrer(ctx context.Context, req *userv1.SearchInviteReferrerRequest) (*userv1.SearchInviteReferrerResponse, error) BindInviteReferrer(ctx context.Context, req *userv1.BindInviteReferrerRequest) (*userv1.BindInviteReferrerResponse, error) } @@ -392,6 +393,10 @@ func (c *grpcUserInviteClient) SearchInviteReferrer(ctx context.Context, req *us return c.client.SearchInviteReferrer(ctx, req) } +func (c *grpcUserInviteClient) GetInviteAttribution(ctx context.Context, req *userv1.GetInviteAttributionRequest) (*userv1.GetInviteAttributionResponse, error) { + return c.client.GetInviteAttribution(ctx, req) +} + func (c *grpcUserInviteClient) BindInviteReferrer(ctx context.Context, req *userv1.BindInviteReferrerRequest) (*userv1.BindInviteReferrerResponse, error) { return c.client.BindInviteReferrer(ctx, req) } diff --git a/services/gateway-service/internal/transport/http/httproutes/router.go b/services/gateway-service/internal/transport/http/httproutes/router.go index c815132a..6d872590 100644 --- a/services/gateway-service/internal/transport/http/httproutes/router.go +++ b/services/gateway-service/internal/transport/http/httproutes/router.go @@ -73,6 +73,7 @@ type UserHandlers struct { BatchRoomDisplayProfiles http.HandlerFunc GetMyOverview http.HandlerFunc GetMyInviteOverview http.HandlerFunc + GetMyInviteReferrer http.HandlerFunc SearchInviteReferrer http.HandlerFunc BindInviteReferrer http.HandlerFunc GetMyGiftWall http.HandlerFunc @@ -422,6 +423,7 @@ func (r routes) registerUserRoutes() { r.profile("/users/room-display-profiles:batch", http.MethodGet, h.BatchRoomDisplayProfiles) r.profile("/users/me/overview", "", h.GetMyOverview) r.profile("/users/me/invite-overview", http.MethodGet, h.GetMyInviteOverview) + r.profile("/users/me/invite-referrer", http.MethodGet, h.GetMyInviteReferrer) r.profile("/users/me/invite-referrer/search", http.MethodGet, h.SearchInviteReferrer) r.profile("/users/me/invite-referrer/bind", http.MethodPost, h.BindInviteReferrer) r.profile("/users/me/gift-wall", http.MethodGet, h.GetMyGiftWall) diff --git a/services/gateway-service/internal/transport/http/userapi/handler.go b/services/gateway-service/internal/transport/http/userapi/handler.go index 073791d0..29bf9f4c 100644 --- a/services/gateway-service/internal/transport/http/userapi/handler.go +++ b/services/gateway-service/internal/transport/http/userapi/handler.go @@ -58,6 +58,7 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers { ResolveDisplayUserID: h.resolveDisplayUserID, GetMyOverview: h.getMyOverview, GetMyInviteOverview: h.getMyInviteOverview, + GetMyInviteReferrer: h.getMyInviteReferrer, SearchInviteReferrer: h.searchInviteReferrer, BindInviteReferrer: h.bindInviteReferrer, GetMyIdentity: h.getMyIdentity, diff --git a/services/gateway-service/internal/transport/http/userapi/invite_referrer_handler.go b/services/gateway-service/internal/transport/http/userapi/invite_referrer_handler.go index 32aef73e..41f8ea28 100644 --- a/services/gateway-service/internal/transport/http/userapi/invite_referrer_handler.go +++ b/services/gateway-service/internal/transport/http/userapi/invite_referrer_handler.go @@ -14,6 +14,55 @@ type bindInviteReferrerData struct { Inviter userProfileData `json:"inviter"` } +type currentInviteReferrerData struct { + Invite *inviteBindingData `json:"invite,omitempty"` + Inviter *userProfileData `json:"inviter,omitempty"` +} + +// getMyInviteReferrer 返回当前用户已经绑定的邀请人;未绑定时返回空对象,让 H5 保持搜索入口。 +func (h *Handler) getMyInviteReferrer(writer http.ResponseWriter, request *http.Request) { + if h.userInviteClient == nil { + httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") + return + } + currentUserID := auth.UserIDFromContext(request.Context()) + + // 绑定关系以 user-service 的归因表为准,前端不能根据最近一次搜索或本地缓存推断是否已经绑定。 + attributionResp, err := h.userInviteClient.GetInviteAttribution(request.Context(), &userv1.GetInviteAttributionRequest{ + Meta: httpkit.UserMeta(request, ""), + InvitedUserId: currentUserID, + }) + if err != nil { + httpkit.WriteRPCError(writer, request, err) + return + } + attribution := attributionResp.GetAttribution() + if attribution == nil || !attribution.GetFound() || strings.TrimSpace(attribution.GetInviteCode()) == "" { + httpkit.WriteOK(writer, request, currentInviteReferrerData{}) + return + } + + // 归因表只保存绑定事实和邀请码;展示资料继续走同一个邀请码搜索校验,保证国家、自邀等展示过滤规则和绑定前预览一致。 + searchResp, err := h.userInviteClient.SearchInviteReferrer(request.Context(), &userv1.SearchInviteReferrerRequest{ + Meta: httpkit.UserMeta(request, ""), + UserId: currentUserID, + InviteCode: attribution.GetInviteCode(), + }) + if err != nil { + httpkit.WriteRPCError(writer, request, err) + return + } + inviter := profileData(searchResp.GetInviter(), 0) + httpkit.WriteOK(writer, request, currentInviteReferrerData{ + Invite: &inviteBindingData{ + Bound: true, + InviteCode: attribution.GetInviteCode(), + InviterUserID: userIDString(attribution.GetInviterUserId()), + }, + Inviter: &inviter, + }) +} + // searchInviteReferrer 只按邀请码暴露当前用户同国家、可绑定的邀请人资料。 func (h *Handler) searchInviteReferrer(writer http.ResponseWriter, request *http.Request) { if h.userInviteClient == nil { diff --git a/services/gateway-service/internal/transport/http/userapi/invite_referrer_handler_test.go b/services/gateway-service/internal/transport/http/userapi/invite_referrer_handler_test.go index f097fa4e..dfced2a2 100644 --- a/services/gateway-service/internal/transport/http/userapi/invite_referrer_handler_test.go +++ b/services/gateway-service/internal/transport/http/userapi/invite_referrer_handler_test.go @@ -13,8 +13,18 @@ import ( ) type fakeInviteReferrerClient struct { - lastSearch *userv1.SearchInviteReferrerRequest - lastBind *userv1.BindInviteReferrerRequest + attribution *userv1.InviteAttribution + lastGet *userv1.GetInviteAttributionRequest + lastSearch *userv1.SearchInviteReferrerRequest + lastBind *userv1.BindInviteReferrerRequest +} + +func (f *fakeInviteReferrerClient) GetInviteAttribution(_ context.Context, req *userv1.GetInviteAttributionRequest) (*userv1.GetInviteAttributionResponse, error) { + f.lastGet = req + if f.attribution == nil { + return &userv1.GetInviteAttributionResponse{Attribution: &userv1.InviteAttribution{}}, nil + } + return &userv1.GetInviteAttributionResponse{Attribution: f.attribution}, nil } func (f *fakeInviteReferrerClient) SearchInviteReferrer(_ context.Context, req *userv1.SearchInviteReferrerRequest) (*userv1.SearchInviteReferrerResponse, error) { @@ -46,6 +56,57 @@ func (f *fakeInviteReferrerClient) BindInviteReferrer(_ context.Context, req *us }, nil } +func TestInviteReferrerGetReturnsBoundInviter(t *testing.T) { + client := &fakeInviteReferrerClient{ + attribution: &userv1.InviteAttribution{ + Found: true, + InvitedUserId: 10001, + InviterUserId: 9001, + InviteCode: "A1B2C3", + }, + } + handler := New(Config{UserInviteClient: client}) + request := newInviteReferrerRequest(http.MethodGet, "/api/v1/users/me/invite-referrer", "") + recorder := httptest.NewRecorder() + + handler.getMyInviteReferrer(recorder, request) + + if recorder.Code != http.StatusOK { + t.Fatalf("get status = %d body=%s", recorder.Code, recorder.Body.String()) + } + if client.lastGet == nil || client.lastGet.GetInvitedUserId() != 10001 { + t.Fatalf("get request mismatch: %+v", client.lastGet) + } + if client.lastSearch == nil || client.lastSearch.GetUserId() != 10001 || client.lastSearch.GetInviteCode() != "A1B2C3" { + t.Fatalf("search request mismatch: %+v", client.lastSearch) + } + if !strings.Contains(recorder.Body.String(), `"invite_code":"A1B2C3"`) || !strings.Contains(recorder.Body.String(), `"username":"Alice"`) { + t.Fatalf("bound inviter response mismatch: %s", recorder.Body.String()) + } +} + +func TestInviteReferrerGetReturnsEmptyWhenUnbound(t *testing.T) { + client := &fakeInviteReferrerClient{} + handler := New(Config{UserInviteClient: client}) + request := newInviteReferrerRequest(http.MethodGet, "/api/v1/users/me/invite-referrer", "") + recorder := httptest.NewRecorder() + + handler.getMyInviteReferrer(recorder, request) + + if recorder.Code != http.StatusOK { + t.Fatalf("get status = %d body=%s", recorder.Code, recorder.Body.String()) + } + if client.lastGet == nil || client.lastGet.GetInvitedUserId() != 10001 { + t.Fatalf("get request mismatch: %+v", client.lastGet) + } + if client.lastSearch != nil { + t.Fatalf("unbound relation must not search inviter: %+v", client.lastSearch) + } + if strings.Contains(recorder.Body.String(), `"inviter"`) || strings.Contains(recorder.Body.String(), `"invite"`) { + t.Fatalf("unbound response should not include inviter data: %s", recorder.Body.String()) + } +} + func TestInviteReferrerSearchUsesInviteCode(t *testing.T) { client := &fakeInviteReferrerClient{} handler := New(Config{UserInviteClient: client})