修复banner

This commit is contained in:
zhx 2026-06-24 12:58:19 +08:00
parent dba6035fbe
commit 9c39d33bf2
4 changed files with 120 additions and 24 deletions

View File

@ -10,6 +10,8 @@ import (
"hyapp/services/gateway-service/internal/appconfig" "hyapp/services/gateway-service/internal/appconfig"
"hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/transport/http/httpkit" "hyapp/services/gateway-service/internal/transport/http/httpkit"
userv1 "hyapp.local/api/proto/user/v1"
) )
type appBootstrapBottomTab struct { type appBootstrapBottomTab struct {
@ -128,13 +130,16 @@ func (h *Handler) listAppBanners(writer http.ResponseWriter, request *http.Reque
return return
} }
items, err := h.appConfigReader.ListBanners(request.Context(), appconfig.BannerQuery{ query := appconfig.BannerQuery{
AppCode: appcode.FromContext(request.Context()), AppCode: appcode.FromContext(request.Context()),
DisplayScope: displayScope, DisplayScope: displayScope,
Platform: httpkit.FirstQueryOrHeader(request, "platform", "X-App-Platform", "X-Platform"), Platform: httpkit.FirstQueryOrHeader(request, "platform", "X-App-Platform", "X-Platform"),
RegionID: optionalInt64Query(request, "region_id"), }
Country: httpkit.FirstQueryOrHeader(request, "country", "X-Country-Code", "X-App-Country"), if !h.applyAuthenticatedBannerAudience(writer, request, &query) {
}) return
}
items, err := h.appConfigReader.ListBanners(request.Context(), query)
if err != nil { if err != nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return return
@ -143,6 +148,44 @@ func (h *Handler) listAppBanners(writer http.ResponseWriter, request *http.Reque
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)}) httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
} }
func (h *Handler) applyAuthenticatedBannerAudience(writer http.ResponseWriter, request *http.Request, query *appconfig.BannerQuery) bool {
viewerUserID := auth.UserIDFromContext(request.Context())
if viewerUserID == 0 {
// banner 是公开接口,但未登录请求不能用 region_id/country 伪装区域身份;空区域只会命中全局 banner。
return true
}
if h.userProfileClient == nil {
// 登录态区域必须以 user-service 的用户事实为准,缺少依赖时不能退回客户端传参。
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return false
}
resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: httpkit.UserMeta(request, ""),
UserId: viewerUserID,
})
if err != nil {
// user-service 查询失败时返回上游错误,避免用客户端 region_id/country 打开错误区域的 banner。
httpkit.WriteRPCError(writer, request, err)
return false
}
user := resp.GetUser()
if user == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return false
}
if user.GetAppCode() != "" && appcode.Normalize(user.GetAppCode()) != query.AppCode {
// token 所属租户和用户资料租户不一致时拒绝返回区域配置,防止跨 App 读取 banner 策略。
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
return false
}
// 已登录用户的区域由后端用户资料覆盖;客户端保留 region_id/country 只做老版本参数兼容,不参与裁决。
query.RegionID = user.GetRegionId()
query.Country = strings.TrimSpace(user.GetCountry())
return true
}
// listSplashScreens 返回后台 APP配置/开屏配置 中当前可见的开屏列表;客户端按列表顺序取最高优先级配置展示。 // listSplashScreens 返回后台 APP配置/开屏配置 中当前可见的开屏列表;客户端按列表顺序取最高优先级配置展示。
func (h *Handler) listSplashScreens(writer http.ResponseWriter, request *http.Request) { func (h *Handler) listSplashScreens(writer http.ResponseWriter, request *http.Request) {
if h.appConfigReader == nil { if h.appConfigReader == nil {

View File

@ -28,25 +28,28 @@ type ObjectUploader interface {
// Handler owns app platform endpoints such as bootstrap, config, upload and device token binding. // Handler owns app platform endpoints such as bootstrap, config, upload and device token binding.
// It does not own user identity or resource semantics; those remain in their owner services/packages. // It does not own user identity or resource semantics; those remain in their owner services/packages.
type Handler struct { type Handler struct {
appConfigReader ConfigReader appConfigReader ConfigReader
userAuthClient client.UserAuthClient userAuthClient client.UserAuthClient
userDeviceClient client.UserDeviceClient userProfileClient client.UserProfileClient
objectUploader ObjectUploader userDeviceClient client.UserDeviceClient
objectUploader ObjectUploader
} }
type Config struct { type Config struct {
AppConfigReader ConfigReader AppConfigReader ConfigReader
UserAuthClient client.UserAuthClient UserAuthClient client.UserAuthClient
UserDeviceClient client.UserDeviceClient UserProfileClient client.UserProfileClient
ObjectUploader ObjectUploader UserDeviceClient client.UserDeviceClient
ObjectUploader ObjectUploader
} }
func New(config Config) *Handler { func New(config Config) *Handler {
return &Handler{ return &Handler{
appConfigReader: config.AppConfigReader, appConfigReader: config.AppConfigReader,
userAuthClient: config.UserAuthClient, userAuthClient: config.UserAuthClient,
userDeviceClient: config.UserDeviceClient, userProfileClient: config.UserProfileClient,
objectUploader: config.ObjectUploader, userDeviceClient: config.UserDeviceClient,
objectUploader: config.ObjectUploader,
} }
} }

View File

@ -3787,7 +3787,8 @@ func (r *captureAppConfigReader) ListPopups(ctx context.Context, query appconfig
} }
func TestListAppBannersReturnsAdminAppConfig(t *testing.T) { func TestListAppBannersReturnsAdminAppConfig(t *testing.T) {
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}) profileClient := &fakeUserProfileClient{}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
reader := &captureAppConfigReader{StaticReader: appconfig.StaticReader{Banners: []appconfig.Banner{ reader := &captureAppConfigReader{StaticReader: appconfig.StaticReader{Banners: []appconfig.Banner{
{ {
ID: 8, ID: 8,
@ -3799,8 +3800,6 @@ func TestListAppBannersReturnsAdminAppConfig(t *testing.T) {
Param: "https://h5.example.com/activity", Param: "https://h5.example.com/activity",
Platform: "android", Platform: "android",
SortOrder: 10, SortOrder: 10,
RegionID: 1,
CountryCode: "CN",
Description: "room banner", Description: "room banner",
StartsAtMs: 1700000000000, StartsAtMs: 1700000000000,
EndsAtMs: 1800000000000, EndsAtMs: 1800000000000,
@ -3827,9 +3826,12 @@ func TestListAppBannersReturnsAdminAppConfig(t *testing.T) {
if response.Code != httpkit.CodeOK || !ok { if response.Code != httpkit.CodeOK || !ok {
t.Fatalf("unexpected envelope: %+v", response) t.Fatalf("unexpected envelope: %+v", response)
} }
if reader.bannerQuery.DisplayScope != "me" || reader.bannerQuery.Platform != "android" || reader.bannerQuery.RegionID != 1 || reader.bannerQuery.Country != "CN" { if reader.bannerQuery.DisplayScope != "me" || reader.bannerQuery.Platform != "android" || reader.bannerQuery.RegionID != 0 || reader.bannerQuery.Country != "" {
t.Fatalf("banner query mismatch: %+v", reader.bannerQuery) t.Fatalf("banner query mismatch: %+v", reader.bannerQuery)
} }
if profileClient.lastGet != nil {
t.Fatalf("anonymous banner request must not query user profile: %+v", profileClient.lastGet)
}
if data["total"].(float64) != 1 { if data["total"].(float64) != 1 {
t.Fatalf("banner total mismatch: %+v", data) t.Fatalf("banner total mismatch: %+v", data)
} }
@ -3853,6 +3855,53 @@ func TestListAppBannersReturnsAdminAppConfig(t *testing.T) {
} }
} }
func TestListAppBannersUsesAuthenticatedUserRegion(t *testing.T) {
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
42: {
UserId: 42,
AppCode: "lalu",
RegionId: 1,
Country: "AE",
},
}}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
reader := &captureAppConfigReader{StaticReader: appconfig.StaticReader{Banners: []appconfig.Banner{
{
ID: 16,
CoverURL: "https://cdn.example.com/middle-east-banner.png",
BannerType: "h5",
DisplayScope: "home",
Param: "https://h5.example.com/middle-east",
Platform: "android",
SortOrder: 1,
RegionID: 1,
CountryCode: "AE",
},
}}}
handler.SetAppConfigReader(reader)
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/app/banners?display_scope=home&platform=android&country=US&region_id=999", nil)
request.Header.Set("X-Request-ID", "req-app-banners-auth")
request.Header.Set("X-App-Code", "lalu")
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
}
if profileClient.lastGet == nil || profileClient.lastGet.GetUserId() != 42 {
t.Fatalf("user profile request mismatch: %+v", profileClient.lastGet)
}
if profileClient.lastGet.GetMeta().GetAppCode() != "lalu" {
t.Fatalf("user profile app_code mismatch: %+v", profileClient.lastGet.GetMeta())
}
if reader.bannerQuery.DisplayScope != "home" || reader.bannerQuery.Platform != "android" || reader.bannerQuery.RegionID != 1 || reader.bannerQuery.Country != "AE" {
t.Fatalf("banner query mismatch: %+v", reader.bannerQuery)
}
}
func TestListSplashScreensReturnsAdminAppConfig(t *testing.T) { func TestListSplashScreensReturnsAdminAppConfig(t *testing.T) {
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}) handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
reader := &captureAppConfigReader{StaticReader: appconfig.StaticReader{SplashScreens: []appconfig.SplashScreen{ reader := &captureAppConfigReader{StaticReader: appconfig.StaticReader{SplashScreens: []appconfig.SplashScreen{

View File

@ -107,10 +107,11 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
GrowthLevelClient: h.growthLevelClient, GrowthLevelClient: h.growthLevelClient,
}) })
appAPI := appapi.New(appapi.Config{ appAPI := appapi.New(appapi.Config{
AppConfigReader: h.appConfigReader, AppConfigReader: h.appConfigReader,
UserAuthClient: h.userClient, UserAuthClient: h.userClient,
UserDeviceClient: h.userDeviceClient, UserProfileClient: h.userProfileClient,
ObjectUploader: h.objectUploader, UserDeviceClient: h.userDeviceClient,
ObjectUploader: h.objectUploader,
}) })
callbackAPI := callbackapi.New(callbackapi.Config{ callbackAPI := callbackapi.New(callbackapi.Config{
RoomClient: h.roomClient, RoomClient: h.roomClient,