修复banner
This commit is contained in:
parent
dba6035fbe
commit
9c39d33bf2
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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®ion_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{
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user