diff --git a/app/api/web/ads_click.go b/app/api/web/ads_click.go new file mode 100644 index 0000000..08e9636 --- /dev/null +++ b/app/api/web/ads_click.go @@ -0,0 +1,74 @@ +package web + +import ( + "net/http" + "videoweb/app/service/configser" + "videoweb/pub/common/log" + + "github.com/gin-gonic/gin" +) + +type adsClickBody struct { + AdsID int64 `json:"adsId"` + Location int64 `json:"location"` +} + +// AdsClick 代理上游 POST /api/app/ads/click;appId 由服务端固定填写。 +func AdsClick(c *gin.Context) { + var body adsClickBody + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 1001, + "msg": "参数错误", + "tip": "参数错误", + "data": nil, + }) + return + } + if body.AdsID <= 0 || body.Location <= 0 { + c.JSON(http.StatusOK, gin.H{ + "code": 1001, + "msg": "参数错误", + "tip": "缺少 adsId 或 location", + "data": nil, + }) + return + } + + env, err := configser.AdsClick(c, configser.AdsClickReq{ + AdsID: body.AdsID, + Location: body.Location, + }) + if err != nil { + log.Error("ads click upstream failed", log.E(err)) + c.JSON(http.StatusOK, gin.H{ + "code": 500, + "msg": "ads click failed", + "tip": "上报失败", + "data": nil, + }) + return + } + if env == nil || env.Code != 200 { + code := 500 + msg := "" + if env != nil { + code = env.Code + msg = env.Msg + } + c.JSON(http.StatusOK, gin.H{ + "code": code, + "msg": msg, + "tip": firstNonEmpty(msg, "上报失败"), + "data": nil, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "msg": env.Msg, + "tip": firstNonEmpty(env.Msg, "success"), + "data": nil, + }) +} diff --git a/app/router/gin.go b/app/router/gin.go index 942aa6b..12f5409 100644 --- a/app/router/gin.go +++ b/app/router/gin.go @@ -38,6 +38,8 @@ func DHInit() *gin.Engine { engine.StaticFile("/favicon.svg", "./static/favicon.svg") engine.StaticFile("/favicon.png", "./static/favicon.png") engine.GET("/ui/dialog/:name", web.DialogFragment) + // 导航页同样展示弹窗 / 九宫格广告,需要点击上报接口。 + engine.POST("/api/app/ads/click", web.AdsClick) engine.GET("/", web.Nav) engine.GET("/nav/", web.Nav) engine.GET("/dh/", web.Nav) @@ -151,6 +153,7 @@ func registerRoutes(r *gin.Engine) { r.POST("/api/contactus/add", web.AddContactUs) r.GET("/api/comment/list", web.CommentList) r.POST("/api/playlet/forward/heat", web.PlayletForwardHeat) + r.POST("/api/app/ads/click", web.AdsClick) r.GET("/api/playlet/chapter/detail", web.ChapterDetail) r.GET("/api/playlet/chapter/list", web.ChapterList) r.GET("/api/playlet/operate/list", web.OperateList) diff --git a/app/service/configser/ads_click.go b/app/service/configser/ads_click.go new file mode 100644 index 0000000..187496b --- /dev/null +++ b/app/service/configser/ads_click.go @@ -0,0 +1,34 @@ +package configser + +import ( + "fmt" + "videoweb/app/service/upstream" + + "github.com/gin-gonic/gin" +) + +const ( + adsClickPath = "/api/app/ads/click" + // AdsClickAppID 广告点击上报的应用 ID,由广告后台分配,固定为 12。 + AdsClickAppID int64 = 12 +) + +// AdsClickReq 对应上游 POST /api/app/ads/click(appId / adsId / location 均必填)。 +type AdsClickReq struct { + AppID int64 `json:"appId"` + AdsID int64 `json:"adsId"` + Location int64 `json:"location"` +} + +// AdsClick 上报一次广告点击。 +func AdsClick(c *gin.Context, req AdsClickReq) (*APIEnvelope[any], error) { + if req.AdsID <= 0 || req.Location <= 0 { + return nil, fmt.Errorf("ads click invalid adsId=%d location=%d", req.AdsID, req.Location) + } + req.AppID = AdsClickAppID + var env APIEnvelope[any] + if _, err := upstream.PostJSON(c, adsClickPath, req, &env); err != nil { + return nil, err + } + return &env, nil +} diff --git a/app/service/configser/view.go b/app/service/configser/view.go index aa0d7a7..0da4551 100644 --- a/app/service/configser/view.go +++ b/app/service/configser/view.go @@ -97,6 +97,20 @@ func buildAdvertisementList(list []Advertisement) []Advertisement { return out } +// ClickID 广告点击上报用的数字 ID(上游下发为数字字符串);非正整数时返回 0。 +func (a Advertisement) ClickID() int64 { + id, err := strconv.ParseInt(strings.TrimSpace(a.ID), 10, 64) + if err != nil || id <= 0 { + return 0 + } + return id +} + +// ClickTrackable 广告 ID 与广告位均有效时才输出点击上报数据(data-ads-id / data-ads-location)。 +func (a Advertisement) ClickTrackable() bool { + return a.ClickID() > 0 && a.Location > 0 +} + // 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 46f1005..d32290d 100644 --- a/app/service/configser/view_test.go +++ b/app/service/configser/view_test.go @@ -80,3 +80,27 @@ func TestBuildAppConfigVMIncludesValidAdvertisements(t *testing.T) { t.Fatalf("DialogList[0] = %#v", got) } } + +func TestAdvertisementClickTrackable(t *testing.T) { + cases := []struct { + ad Advertisement + wantID int64 + trackable bool + }{ + {Advertisement{ID: "1138200002", Location: 2}, 1138200002, true}, + {Advertisement{ID: " 1138200012 ", Location: 12}, 1138200012, true}, + {Advertisement{ID: "1138200003", Location: 0}, 1138200003, false}, + {Advertisement{ID: "", Location: 3}, 0, false}, + {Advertisement{ID: "0", Location: 3}, 0, false}, + {Advertisement{ID: "-5", Location: 3}, 0, false}, + {Advertisement{ID: "abc", Location: 3}, 0, false}, + } + for _, tc := range cases { + if got := tc.ad.ClickID(); got != tc.wantID { + t.Fatalf("ClickID(%q) = %d, want %d", tc.ad.ID, got, tc.wantID) + } + if got := tc.ad.ClickTrackable(); got != tc.trackable { + t.Fatalf("ClickTrackable(%q, %d) = %v, want %v", tc.ad.ID, tc.ad.Location, got, tc.trackable) + } + } +} diff --git a/app/static/js/common/adClick.js b/app/static/js/common/adClick.js new file mode 100644 index 0000000..e416b52 --- /dev/null +++ b/app/static/js/common/adClick.js @@ -0,0 +1,53 @@ +/** + * 广告点击上报:POST /api/app/ads/click(本站 Go 服务代理到上游,appId 由服务端固定为 12)。 + * - 广告元素由服务端模板输出 data-ads-id / data-ads-location,ID 无效时不输出,即不上报 + * - document 捕获阶段监听 click 与 auxclick(中键),不阻止默认行为、不影响广告链接打开 + * - 不依赖 request.js(导航页未加载),失败静默 + */ +(function () { + if (window.__adClickBound) return; + window.__adClickBound = true; + + var ENDPOINT = "/api/app/ads/click"; + + function positiveInt(value) { + var text = String(value == null ? "" : value).trim(); + if (!/^\d+$/.test(text)) return 0; + var number = Number(text); + return Number.isSafeInteger(number) && number > 0 ? number : 0; + } + + function payloadFrom(target) { + if (!target || typeof target.closest !== "function") return null; + var el = target.closest("[data-ads-id]"); + if (!el) return null; + var adsId = positiveInt(el.getAttribute("data-ads-id")); + var location = positiveInt(el.getAttribute("data-ads-location")); + return adsId && location ? { adsId: adsId, location: location } : null; + } + + function report(payload) { + try { + fetch(ENDPOINT, { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify(payload), + credentials: "same-origin", + keepalive: true, + }).catch(function () {}); + } catch (error) { + // 上报失败不影响广告跳转。 + } + } + + function onClick(event) { + if (event.type === "auxclick" && event.button !== 1) return; + var target = event.target; + if (target && target.nodeType !== 1) target = target.parentElement; + var payload = payloadFrom(target); + if (payload) report(payload); + } + + document.addEventListener("click", onClick, true); + document.addEventListener("auxclick", onClick, true); +})(); diff --git a/app/templates/base/contentExtras.html b/app/templates/base/contentExtras.html index 7bede11..b5cfb64 100644 --- a/app/templates/base/contentExtras.html +++ b/app/templates/base/contentExtras.html @@ -40,7 +40,7 @@ {{if .appConfig.DetailBannerList}}
{{range $ad := .appConfig.DetailBannerList}} - + {{$ad.Title}} {{end}} diff --git a/app/templates/base/dialogAd.html b/app/templates/base/dialogAd.html index 408c256..1f7efe5 100644 --- a/app/templates/base/dialogAd.html +++ b/app/templates/base/dialogAd.html @@ -20,11 +20,11 @@ hidden > {{if $dialog.Link}} - + {{$dialog.Title}} {{else}} -
+
{{$dialog.Title}}
{{end}} diff --git a/app/templates/base/header.html b/app/templates/base/header.html index 70ce3f3..3a46a6e 100644 --- a/app/templates/base/header.html +++ b/app/templates/base/header.html @@ -166,6 +166,7 @@
+ diff --git a/app/templates/base/nineGridDialog.html b/app/templates/base/nineGridDialog.html index e49de7f..1928514 100644 --- a/app/templates/base/nineGridDialog.html +++ b/app/templates/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.ClickTrackable}}data-ads-id="{{$ad.ClickID}}" data-ads-location="{{$ad.Location}}"{{end}} hidden > {{$ad.Title}} diff --git a/app/templates/base/squarePromo.html b/app/templates/base/squarePromo.html index 3c7aa95..7605bf9 100644 --- a/app/templates/base/squarePromo.html +++ b/app/templates/base/squarePromo.html @@ -18,6 +18,7 @@ {{else}}aria-disabled="true" {{end}} data-creative-id="{{$ad.ID}}" + {{if $ad.ClickTrackable}}data-ads-id="{{$ad.ClickID}}" data-ads-location="{{$ad.Location}}"{{end}} >
+ {{template "dialogAd" .}} {{template "nineGridDialog" .}}