广告点击添加
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -40,7 +40,7 @@
|
||||
{{if .appConfig.DetailBannerList}}
|
||||
<div class="content-ad-stack" aria-label="广告">
|
||||
{{range $ad := .appConfig.DetailBannerList}}
|
||||
<a {{if $ad.Link}}href="{{$ad.Link}}" target="_blank" rel="sponsored nofollow noopener noreferrer"{{else}}aria-disabled="true"{{end}} data-creative-id="{{$ad.ID}}">
|
||||
<a {{if $ad.Link}}href="{{$ad.Link}}" target="_blank" rel="sponsored nofollow noopener noreferrer"{{else}}aria-disabled="true"{{end}} data-creative-id="{{$ad.ID}}"{{if $ad.ClickTrackable}} data-ads-id="{{$ad.ClickID}}" data-ads-location="{{$ad.Location}}"{{end}}>
|
||||
<img class="lazy" src="/static/images/placeholder.png?v=2" data-src="{{$ad.Images}}" alt="{{$ad.Title}}" title="{{$ad.Title}}" />
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
hidden
|
||||
>
|
||||
{{if $dialog.Link}}
|
||||
<a class="dialog-ad-media" href="{{$dialog.Link}}" target="_blank" rel="sponsored nofollow noopener noreferrer" aria-label="{{if $dialog.Title}}{{$dialog.Title}}{{else}}查看广告{{end}}" data-creative-id="{{$dialog.ID}}">
|
||||
<a class="dialog-ad-media" href="{{$dialog.Link}}" target="_blank" rel="sponsored nofollow noopener noreferrer" aria-label="{{if $dialog.Title}}{{$dialog.Title}}{{else}}查看广告{{end}}" data-creative-id="{{$dialog.ID}}"{{if $dialog.ClickTrackable}} data-ads-id="{{$dialog.ClickID}}" data-ads-location="{{$dialog.Location}}"{{end}}>
|
||||
<img class="dialog-ad-image" src="/static/images/placeholder.png?v=2" data-dialog-ad-src="{{$dialog.Images}}" alt="{{$dialog.Title}}" title="{{$dialog.Title}}" decoding="async" />
|
||||
</a>
|
||||
{{else}}
|
||||
<div class="dialog-ad-media">
|
||||
<div class="dialog-ad-media"{{if $dialog.ClickTrackable}} data-ads-id="{{$dialog.ClickID}}" data-ads-location="{{$dialog.Location}}"{{end}}>
|
||||
<img class="dialog-ad-image" src="/static/images/placeholder.png?v=2" data-dialog-ad-src="{{$dialog.Images}}" alt="{{$dialog.Title}}" title="{{$dialog.Title}}" decoding="async" />
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
</div>
|
||||
|
||||
<script defer src="/static/js/common/request.js?v=3"></script>
|
||||
<script defer src="/static/js/common/adClick.js?v=1"></script>
|
||||
<script defer src="/static/common/imgDecypt.js?v=2"></script>
|
||||
<script defer src="/static/js/pages/header.js?v=10"></script>
|
||||
<script defer src="/static/js/pages/dialogLoader.js?v=7"></script>
|
||||
|
||||
@@ -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
|
||||
>
|
||||
<img class="nine-grid-icon" src="/static/images/placeholder.png?v=2" data-nine-grid-src="{{$ad.Images}}" alt="{{$ad.Title}}" title="{{$ad.Title}}" decoding="async" />
|
||||
|
||||
@@ -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}}
|
||||
>
|
||||
<span class="square-promo__cover">
|
||||
<img
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
{{template "pageConsole" .}}
|
||||
<noscript><div><img src="https://mc.yandex.ru/watch/112390883" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||
<script defer src="/static/common/imgDecypt.js?v=2"></script>
|
||||
<script defer src="/static/js/common/adClick.js?v=1"></script>
|
||||
{{template "dialogAd" .}}
|
||||
{{template "nineGridDialog" .}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user