广告点击添加
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)。
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user