feat: expose current invite referrer

This commit is contained in:
zhx 2026-07-01 14:37:24 +08:00
parent 4b02562864
commit cb8cfd2167
5 changed files with 120 additions and 2 deletions

View File

@ -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)
}

View File

@ -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)

View File

@ -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,

View File

@ -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 {

View File

@ -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})