修复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/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 {
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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®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) {
|
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{
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user