From 8be05db0e6ff011fb6c1d45ad4be12f422c23b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E5=AE=87=E5=AE=81?= Date: Thu, 29 Oct 2026 16:41:17 +0700 Subject: [PATCH] =?UTF-8?q?=E5=B9=BF=E5=91=8A=E7=82=B9=E5=87=BB=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/web/adsClick.go | 56 +++++++++++++++++++++++++++ app/router/gin.go | 3 ++ app/service/adsser/click.go | 37 ++++++++++++++++++ app/service/configser/types.go | 7 ++++ app/service/configser/view.go | 10 +++++ app/service/configser/view_test.go | 27 +++++++++++++ app/static/js/common/adsClick.js | 48 +++++++++++++++++++++++ app/template/base/ads.html | 2 +- app/template/base/appIcons.html | 2 +- app/template/base/dialogAd.html | 4 +- app/template/base/header.html | 1 + app/template/base/nineGridDialog.html | 1 + 12 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 app/api/web/adsClick.go create mode 100644 app/service/adsser/click.go create mode 100644 app/static/js/common/adsClick.js diff --git a/app/api/web/adsClick.go b/app/api/web/adsClick.go new file mode 100644 index 0000000..c5819a7 --- /dev/null +++ b/app/api/web/adsClick.go @@ -0,0 +1,56 @@ +package web + +import ( + "net/http" + "videoweb/app/service/adsser" + + "github.com/gin-gonic/gin" +) + +// adsClickBody 浏览器上报体;appId 由服务端固定,不信任前端。 +type adsClickBody struct { + AdsId int64 `json:"adsId"` + Location int64 `json:"location"` +} + +// AdsClick BFF:代理 POST /api/app/ads/click(广告点击上报)。 +func AdsClick(c *gin.Context) { + var body adsClickBody + if err := c.ShouldBindJSON(&body); err != nil || body.AdsId <= 0 || body.Location <= 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "invalid body", + "tip": "参数错误", + "data": nil, + }) + return + } + + env, err := adsser.Click(c, adsser.ClickReq{ + AppId: adsser.ClickAppID, + AdsId: body.AdsId, + Location: body.Location, + }) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "code": 500, + "msg": err.Error(), + "data": nil, + }) + return + } + if env == nil { + c.JSON(http.StatusOK, gin.H{ + "code": 500, + "msg": "empty response", + "data": nil, + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "code": env.Code, + "msg": env.Msg, + "tip": env.Tip, + "data": env.Data, + }) +} diff --git a/app/router/gin.go b/app/router/gin.go index 652752b..393860e 100644 --- a/app/router/gin.go +++ b/app/router/gin.go @@ -203,6 +203,9 @@ func registerRoutes(r *gin.Engine) { r.POST("/api/upload/image", web.UploadImage) r.POST("/api/feedback/add", web.FeedbackAdd) + // 广告点击上报 BFF:代理 POST /api/app/ads/click + r.POST("/api/ads/click", web.AdsClick) + // 播放页 BFF r.GET("/api/playlet/guess-like/list", web.GuessLikeList) r.GET("/api/web/search/suggest", web.SearchSuggest) diff --git a/app/service/adsser/click.go b/app/service/adsser/click.go new file mode 100644 index 0000000..6dfa60f --- /dev/null +++ b/app/service/adsser/click.go @@ -0,0 +1,37 @@ +package adsser + +import ( + "encoding/json" + "videoweb/app/service/upstream" + + "github.com/gin-gonic/gin" +) + +const clickPath = "/api/app/ads/click" + +// ClickAppID 广告点击上报固定 appId。 +const ClickAppID int64 = 12 + +// ClickReq 对应上游 AdsClickInfo:appId / adsId / location 均必填。 +type ClickReq struct { + AppId int64 `json:"appId"` + AdsId int64 `json:"adsId"` + Location int64 `json:"location"` +} + +// APIEnvelope 上游通用响应。 +type APIEnvelope struct { + Code int `json:"code"` + Msg string `json:"msg"` + Tip string `json:"tip"` + Data json.RawMessage `json:"data"` +} + +// Click 调用上游上报广告点击。 +func Click(c *gin.Context, req ClickReq) (*APIEnvelope, error) { + var env APIEnvelope + if _, err := upstream.PostJSON(c, clickPath, req, &env); err != nil { + return nil, err + } + return &env, nil +} diff --git a/app/service/configser/types.go b/app/service/configser/types.go index 34a1db4..5f81654 100644 --- a/app/service/configser/types.go +++ b/app/service/configser/types.go @@ -122,6 +122,13 @@ type Advertisement struct { AdvertiseCode string `json:"advertiseCode"` AdvertiseLocationCode string `json:"advertiseLocationCode"` AdvertiseLocationName string `json:"advertiseLocationName"` + // AdsID 点击上报用的广告 id(上游 id 为数字字符串),在 advertiseCode 覆盖 ID 之前解析;无效为 0。 + AdsID int64 `json:"-"` +} + +// CanReportClick 广告 id 与 location 均为正整数时才上报点击(POST /api/app/ads/click)。 +func (a Advertisement) CanReportClick() bool { + return a.AdsID > 0 && a.Location > 0 } // NewAddressInfo 对应 swagger proto.NewAddressInfo(GET /api/app/config/info)。 diff --git a/app/service/configser/view.go b/app/service/configser/view.go index e7bd8bc..eddfd8a 100644 --- a/app/service/configser/view.go +++ b/app/service/configser/view.go @@ -131,6 +131,7 @@ func buildAdvertisementList(list []Advertisement) []Advertisement { item.AdvertiseCode = strings.TrimSpace(item.AdvertiseCode) item.AdvertiseLocationCode = strings.TrimSpace(item.AdvertiseLocationCode) item.AdvertiseLocationName = strings.TrimSpace(item.AdvertiseLocationName) + item.AdsID = parseAdsID(item.ID) if item.AdvertiseCode != "" { item.ID = item.AdvertiseCode } @@ -145,6 +146,15 @@ func buildAdvertisementList(list []Advertisement) []Advertisement { return out } +// parseAdsID 解析上游数字字符串广告 id;非正整数返回 0(不上报)。 +func parseAdsID(id string) int64 { + n, err := strconv.ParseInt(strings.TrimSpace(id), 10, 64) + if err != nil || n <= 0 { + return 0 + } + return n +} + // firstAdvertisementList 返回第一个有可用广告的列表(按字段优先级)。 func firstAdvertisementList(lists ...[]Advertisement) []Advertisement { for _, list := range lists { diff --git a/app/service/configser/view_test.go b/app/service/configser/view_test.go index 842032f..a2182ef 100644 --- a/app/service/configser/view_test.go +++ b/app/service/configser/view_test.go @@ -45,3 +45,30 @@ func TestBuildAppConfigVMFallsBackToLegacyBannerField(t *testing.T) { t.Fatalf("DetailBannerList = %#v, want detailbannerList fallback", vm.DetailBannerList) } } + +func TestBuildAppConfigVMParsesAdsClickID(t *testing.T) { + vm := BuildAppConfigVM(&ConfigInfoResp{ + Advitise: AdvitiseData{ + NineGridList: []Advertisement{ + {ID: " 1138200002 ", Location: 2, Images: "https://cdn.example.com/a.gif"}, + {ID: "1138200003", Location: 2, AdvertiseCode: "code-x", Images: "https://cdn.example.com/b.gif"}, + {ID: "abc", Location: 2, Images: "https://cdn.example.com/c.gif"}, + {ID: "1138200004", Location: 0, Images: "https://cdn.example.com/d.gif"}, + }, + }, + }) + got := vm.NineGridList + if len(got) != 4 { + t.Fatalf("NineGridList = %#v", got) + } + if got[0].AdsID != 1138200002 || !got[0].CanReportClick() { + t.Fatalf("item0 = %#v, want reportable id 1138200002", got[0]) + } + // advertiseCode 覆盖展示 ID,但点击上报仍用上游原始 id + if got[1].ID != "code-x" || got[1].AdsID != 1138200003 { + t.Fatalf("item1 = %#v, want AdsID from raw id", got[1]) + } + if got[2].CanReportClick() || got[3].CanReportClick() { + t.Fatalf("invalid id/location must not report: %#v %#v", got[2], got[3]) + } +} diff --git a/app/static/js/common/adsClick.js b/app/static/js/common/adsClick.js new file mode 100644 index 0000000..918c113 --- /dev/null +++ b/app/static/js/common/adsClick.js @@ -0,0 +1,48 @@ +/** + * 广告点击上报:点击任一带 data-ads-id / data-ads-location 的广告元素时, + * 异步 POST 本站 BFF /api/ads/click(服务端补 appId 并转发 /api/app/ads/click)。 + * - 捕获阶段监听 click / auxclick(中键),不 preventDefault,不阻塞广告链接打开 + * - id、location 均为正整数才上报;失败静默 + */ +(function () { + var ENDPOINT = "/api/ads/click"; + + function toPositiveInt(value) { + var text = String(value == null ? "" : value).trim(); + if (!/^\d+$/.test(text)) return 0; + var n = Number(text); + return n > 0 && n <= 9007199254740991 ? n : 0; + } + + function findAdElement(target) { + var el = target && target.nodeType === 1 ? target : target && target.parentElement; + return el && typeof el.closest === "function" ? el.closest("[data-ads-id][data-ads-location]") : null; + } + + function report(adsId, location) { + try { + fetch(ENDPOINT, { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify({ adsId: adsId, location: location }), + credentials: "include", + keepalive: true, + }).catch(function () {}); + } catch (error) { + // 上报失败静默,不影响广告跳转。 + } + } + + function onClick(event) { + if (event.type === "auxclick" && event.button !== 1) return; + var el = findAdElement(event.target); + if (!el) return; + var adsId = toPositiveInt(el.getAttribute("data-ads-id")); + var location = toPositiveInt(el.getAttribute("data-ads-location")); + if (!adsId || !location) return; + report(adsId, location); + } + + document.addEventListener("click", onClick, true); + document.addEventListener("auxclick", onClick, true); +})(); diff --git a/app/template/base/ads.html b/app/template/base/ads.html index 6369517..49c3762 100644 --- a/app/template/base/ads.html +++ b/app/template/base/ads.html @@ -5,7 +5,7 @@
{{range .}} - + {{if .Title}}{{.Title}}{{else}}推荐{{end}} 广告 diff --git a/app/template/base/appIcons.html b/app/template/base/appIcons.html index 8677b39..ce6f612 100644 --- a/app/template/base/appIcons.html +++ b/app/template/base/appIcons.html @@ -7,7 +7,7 @@ 推广
{{range .Items}} - + {{if .Title}}{{.Title}}{{else}}推荐{{end}} {{if .Title}}{{.Title}}{{else}}推荐{{end}} diff --git a/app/template/base/dialogAd.html b/app/template/base/dialogAd.html index 1d13a76..087b789 100644 --- a/app/template/base/dialogAd.html +++ b/app/template/base/dialogAd.html @@ -21,7 +21,7 @@
{{if $dialog.Link}} - + {{$dialog.Title}} {{else}} @@ -41,7 +41,7 @@

每日展示一次

{{if $dialog.Link}} - + 播放 立即前往 diff --git a/app/template/base/header.html b/app/template/base/header.html index 3d4e880..c9c7ade 100644 --- a/app/template/base/header.html +++ b/app/template/base/header.html @@ -158,6 +158,7 @@
+ diff --git a/app/template/base/nineGridDialog.html b/app/template/base/nineGridDialog.html index d3c2126..108e6f6 100644 --- a/app/template/base/nineGridDialog.html +++ b/app/template/base/nineGridDialog.html @@ -11,6 +11,7 @@ {{if $ad.Link}}href="{{$ad.Link}}" target="_blank" rel="sponsored nofollow noopener noreferrer"{{else}}aria-disabled="true"{{end}} data-nine-grid-item data-creative-id="{{$ad.ID}}" + {{if $ad.CanReportClick}}data-ads-id="{{$ad.AdsID}}" data-ads-location="{{$ad.Location}}"{{end}} hidden > {{$ad.Title}}