修复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/auth"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
userv1 "hyapp.local/api/proto/user/v1"
)
type appBootstrapBottomTab struct {
@ -128,13 +130,16 @@ func (h *Handler) listAppBanners(writer http.ResponseWriter, request *http.Reque
return
}
items, err := h.appConfigReader.ListBanners(request.Context(), appconfig.BannerQuery{
query := appconfig.BannerQuery{
AppCode: appcode.FromContext(request.Context()),
DisplayScope: displayScope,
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 {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
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)})
}
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配置/开屏配置 中当前可见的开屏列表;客户端按列表顺序取最高优先级配置展示。
func (h *Handler) listSplashScreens(writer http.ResponseWriter, request *http.Request) {
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.
// It does not own user identity or resource semantics; those remain in their owner services/packages.
type Handler struct {
appConfigReader ConfigReader
userAuthClient client.UserAuthClient
userDeviceClient client.UserDeviceClient
objectUploader ObjectUploader
appConfigReader ConfigReader
userAuthClient client.UserAuthClient
userProfileClient client.UserProfileClient
userDeviceClient client.UserDeviceClient
objectUploader ObjectUploader
}
type Config struct {
AppConfigReader ConfigReader
UserAuthClient client.UserAuthClient
UserDeviceClient client.UserDeviceClient
ObjectUploader ObjectUploader
AppConfigReader ConfigReader
UserAuthClient client.UserAuthClient
UserProfileClient client.UserProfileClient
UserDeviceClient client.UserDeviceClient
ObjectUploader ObjectUploader
}
func New(config Config) *Handler {
return &Handler{
appConfigReader: config.AppConfigReader,
userAuthClient: config.UserAuthClient,
userDeviceClient: config.UserDeviceClient,
objectUploader: config.ObjectUploader,
appConfigReader: config.AppConfigReader,
userAuthClient: config.UserAuthClient,
userProfileClient: config.UserProfileClient,
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) {
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
profileClient := &fakeUserProfileClient{}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
reader := &captureAppConfigReader{StaticReader: appconfig.StaticReader{Banners: []appconfig.Banner{
{
ID: 8,
@ -3799,8 +3800,6 @@ func TestListAppBannersReturnsAdminAppConfig(t *testing.T) {
Param: "https://h5.example.com/activity",
Platform: "android",
SortOrder: 10,
RegionID: 1,
CountryCode: "CN",
Description: "room banner",
StartsAtMs: 1700000000000,
EndsAtMs: 1800000000000,
@ -3827,9 +3826,12 @@ func TestListAppBannersReturnsAdminAppConfig(t *testing.T) {
if response.Code != httpkit.CodeOK || !ok {
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)
}
if profileClient.lastGet != nil {
t.Fatalf("anonymous banner request must not query user profile: %+v", profileClient.lastGet)
}
if data["total"].(float64) != 1 {
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) {
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
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,
})
appAPI := appapi.New(appapi.Config{
AppConfigReader: h.appConfigReader,
UserAuthClient: h.userClient,
UserDeviceClient: h.userDeviceClient,
ObjectUploader: h.objectUploader,
AppConfigReader: h.appConfigReader,
UserAuthClient: h.userClient,
UserProfileClient: h.userProfileClient,
UserDeviceClient: h.userDeviceClient,
ObjectUploader: h.objectUploader,
})
callbackAPI := callbackapi.New(callbackapi.Config{
RoomClient: h.roomClient,