广告点击添加

This commit is contained in:
谢宇宁
2026-10-29 16:41:17 +07:00
parent 9c0695b305
commit 8be05db0e6
12 changed files with 194 additions and 4 deletions
+56
View File
@@ -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,
})
}
+3
View File
@@ -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)
+37
View File
@@ -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
}
+7
View File
@@ -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)。
+10
View File
@@ -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 {
+27
View File
@@ -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])
}
}
+48
View File
@@ -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);
})();
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="stylesheet" href="/static/css/promo.css?v=2">
<section class="promo-rail" aria-label="推荐">
{{range .}}
<a class="promo-card" href="{{if .Link}}{{.Link}}{{else}}#{{end}}"{{if .Link}} target="_blank" rel="sponsored nofollow noopener"{{end}}>
<a class="promo-card" href="{{if .Link}}{{.Link}}{{else}}#{{end}}"{{if .Link}} target="_blank" rel="sponsored nofollow noopener"{{end}}{{if .CanReportClick}} data-ads-id="{{.AdsID}}" data-ads-location="{{.Location}}"{{end}}>
<img decoding="async" loading="eager" class="lazy" src="/static/images/placeholder.png?v=2" data-src="{{.Images}}" data-lazy-eager="1" alt="{{if .Title}}{{.Title}}{{else}}推荐{{end}}">
<span class="tag tag--dark promo-card__tag">广告</span>
</a>
+1 -1
View File
@@ -7,7 +7,7 @@
<span class="app-icons__label">推广</span>
<div class="app-icons__row">
{{range .Items}}
<a class="app-icons__item" href="{{if .Link}}{{.Link}}{{else}}#{{end}}"{{if .Link}} target="_blank" rel="sponsored nofollow noopener"{{end}} title="{{.Title}}">
<a class="app-icons__item" href="{{if .Link}}{{.Link}}{{else}}#{{end}}"{{if .Link}} target="_blank" rel="sponsored nofollow noopener"{{end}} title="{{.Title}}"{{if .CanReportClick}} data-ads-id="{{.AdsID}}" data-ads-location="{{.Location}}"{{end}}>
<img decoding="async" class="lazy app-icons__img" src="/static/images/placeholder.png?v=2" data-src="{{.Images}}" data-lazy-eager="1" alt="{{if .Title}}{{.Title}}{{else}}推荐{{end}}">
<span class="app-icons__name">{{if .Title}}{{.Title}}{{else}}推荐{{end}}</span>
</a>
+2 -2
View File
@@ -21,7 +21,7 @@
<div class="dialog-ad-card">
<div class="dialog-ad-cover">
{{if $dialog.Link}}
<a class="dialog-ad-media" href="{{$dialog.Link}}" target="_blank" rel="sponsored nofollow noopener" aria-label="{{if $dialog.Title}}{{$dialog.Title}}{{else}}查看广告{{end}}" data-dialog-ad-link>
<a class="dialog-ad-media" href="{{$dialog.Link}}" target="_blank" rel="sponsored nofollow noopener" aria-label="{{if $dialog.Title}}{{$dialog.Title}}{{else}}查看广告{{end}}" data-dialog-ad-link{{if $dialog.CanReportClick}} data-ads-id="{{$dialog.AdsID}}" data-ads-location="{{$dialog.Location}}"{{end}}>
<img loading="lazy" 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}}
@@ -41,7 +41,7 @@
<p class="dialog-ad-desc">每日展示一次</p>
</div>
{{if $dialog.Link}}
<a class="dialog-ad-cta" href="{{$dialog.Link}}" target="_blank" rel="sponsored nofollow noopener" data-dialog-ad-link>
<a class="dialog-ad-cta" href="{{$dialog.Link}}" target="_blank" rel="sponsored nofollow noopener" data-dialog-ad-link{{if $dialog.CanReportClick}} data-ads-id="{{$dialog.AdsID}}" data-ads-location="{{$dialog.Location}}"{{end}}>
<img decoding="async" loading="lazy" class="dialog-ad-cta__icon" src="/static/images/placeholder.png?v=2" data-src="/static/images/play_hollow.svg" width="18" height="18" alt="播放">
立即前往
</a>
+1
View File
@@ -158,6 +158,7 @@
</div>
<script defer src="/static/js/common/request.js?v=6"></script>
<script defer src="/static/js/common/adsClick.js?v=1"></script>
<script defer src="/static/common/imgDecypt.js?v=9"></script>
<script defer src="/static/js/header.js?v=13"></script>
+1
View File
@@ -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
>
<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" />