From 01d9f7bc86114ce4b6fa75f9035e5554676a538c Mon Sep 17 00:00:00 2001 From: root Date: Tue, 22 Sep 2026 16:53:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B9=BF=E5=91=8AV2=E6=A8=A1?= =?UTF-8?q?=E5=9D=97,=E5=8F=8C=E7=AB=AF=E4=B8=8B=E8=BD=BD=E8=90=BD?= =?UTF-8?q?=E5=9C=B0=E9=A1=B5=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 advitise v2: info/location/bind/click-stat/day-click-stat 模型与 对应的 service、api、router、proto 定义 - models/index.go 注册 5 个新模型的索引初始化 - 鉴权白名单: /rsdownload/ 改为 /download/,放行 advitise v2 的 get/click 接口 - statg/config: proxyUrl 增加 fileRes1 - common/genId: lid 生成逻辑补充 - 新增 91fans / huangguo 的 mobileconfig 描述文件 - 新增 deploy/nginx/cpcdownload.mfcg.me.conf 网关配置 Co-Authored-By: Claude Opus 5 (1M context) --- api/advitiseV2Api/bind.go | 106 +++++++++ api/advitiseV2Api/get_click.go | 49 +++++ api/advitiseV2Api/info.go | 106 +++++++++ api/advitiseV2Api/location.go | 106 +++++++++ api/advitiseV2Api/stat.go | 80 +++++++ api/fileApi/file.go | 3 +- api/proto/advertise.go | 26 +-- api/proto/advitiseV2.go | 232 ++++++++++++++++++++ api/versionApi/cpcdownload.go | 2 +- common/genId/lid.go | 32 ++- conf/91fans.mobileconfig | 62 ++++++ conf/huangguo.mobileconfig | 62 ++++++ deploy/nginx/cpcdownload.mfcg.me.conf | 58 +++++ middleware/auth/http.go | 4 +- models/advertiseMod/advertise.go | 24 +++ models/advitiseBindMod/bind.go | 78 +++++++ models/advitiseBindMod/tag.go | 30 +++ models/advitiseBindMod/type.go | 22 ++ models/advitiseClickStatMod/stat.go | 99 +++++++++ models/advitiseClickStatMod/tag.go | 30 +++ models/advitiseClickStatMod/type.go | 25 +++ models/advitiseDayClickStatMod/stat.go | 68 ++++++ models/advitiseDayClickStatMod/tag.go | 26 +++ models/advitiseDayClickStatMod/type.go | 23 ++ models/advitiseInfoMod/advitise.go | 79 +++++++ models/advitiseInfoMod/tag.go | 56 +++++ models/advitiseInfoMod/type.go | 35 +++ models/advitiseLocationMod/location.go | 80 +++++++ models/advitiseLocationMod/tag.go | 34 +++ models/advitiseLocationMod/type.go | 38 ++++ models/index.go | 10 + router/advitiseV2.go | 40 ++++ router/base.go | 3 +- service/advertiseSer/advertise.go | 30 ++- service/advitiseV2Ser/bind.go | 92 ++++++++ service/advitiseV2Ser/get_click.go | 214 +++++++++++++++++++ service/advitiseV2Ser/info.go | 144 +++++++++++++ service/advitiseV2Ser/location.go | 93 ++++++++ service/advitiseV2Ser/stat.go | 285 +++++++++++++++++++++++++ statg/config.go | 5 +- 40 files changed, 2561 insertions(+), 30 deletions(-) create mode 100644 api/advitiseV2Api/bind.go create mode 100644 api/advitiseV2Api/get_click.go create mode 100644 api/advitiseV2Api/info.go create mode 100644 api/advitiseV2Api/location.go create mode 100644 api/advitiseV2Api/stat.go create mode 100644 api/proto/advitiseV2.go create mode 100644 conf/91fans.mobileconfig create mode 100644 conf/huangguo.mobileconfig create mode 100644 deploy/nginx/cpcdownload.mfcg.me.conf create mode 100644 models/advitiseBindMod/bind.go create mode 100644 models/advitiseBindMod/tag.go create mode 100644 models/advitiseBindMod/type.go create mode 100644 models/advitiseClickStatMod/stat.go create mode 100644 models/advitiseClickStatMod/tag.go create mode 100644 models/advitiseClickStatMod/type.go create mode 100644 models/advitiseDayClickStatMod/stat.go create mode 100644 models/advitiseDayClickStatMod/tag.go create mode 100644 models/advitiseDayClickStatMod/type.go create mode 100644 models/advitiseInfoMod/advitise.go create mode 100644 models/advitiseInfoMod/tag.go create mode 100644 models/advitiseInfoMod/type.go create mode 100644 models/advitiseLocationMod/location.go create mode 100644 models/advitiseLocationMod/tag.go create mode 100644 models/advitiseLocationMod/type.go create mode 100644 router/advitiseV2.go create mode 100644 service/advitiseV2Ser/bind.go create mode 100644 service/advitiseV2Ser/get_click.go create mode 100644 service/advitiseV2Ser/info.go create mode 100644 service/advitiseV2Ser/location.go create mode 100644 service/advitiseV2Ser/stat.go diff --git a/api/advitiseV2Api/bind.go b/api/advitiseV2Api/bind.go new file mode 100644 index 0000000..22e330a --- /dev/null +++ b/api/advitiseV2Api/bind.go @@ -0,0 +1,106 @@ +package advitiseV2Api + +import ( + "stat/api/proto" + "stat/common/stdLog" + "stat/common/utils" + "stat/service/advitiseV2Ser" + "stat/service/opLogSer" + + "github.com/gin-gonic/gin" +) + +// @Tags 广告管理V2/绑定 +// @Summary 绑定关系列表 +// @Router /api/stat/advitise/v2/bind/list [POST] +// @Param param body proto.AdvitiseBindListReq true "参数" +// @Success 200 {object} proto.AdvitiseBindListResp "成功" +// @Security ApiKeyAuth +func BindList(ctx *gin.Context) { + req := &proto.AdvitiseBindListReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + resp, err := advitiseV2Ser.BindList(ctx, req) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + stdLog.ServerJSON(ctx, stdLog.Success, resp) +} + +// @Tags 广告管理V2/绑定 +// @Summary 新增绑定 +// @Router /api/stat/advitise/v2/bind/add [POST] +// @Param param body proto.AddAdvitiseBindReq true "参数" +// @Success 200 {string} string "成功" +// @Security ApiKeyAuth +func BindAdd(ctx *gin.Context) { + account, err := utils.GetAdmin(ctx) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error()) + return + } + req := &proto.AddAdvitiseBindReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + if err := advitiseV2Ser.BindAdd(ctx, req); err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + opLogSer.AddOpLog(ctx, account, true, req) + stdLog.ServerJSON(ctx, stdLog.Success, "") +} + +// @Tags 广告管理V2/绑定 +// @Summary 更新绑定 +// @Router /api/stat/advitise/v2/bind/update [POST] +// @Param param body proto.UpdateAdvitiseBindReq true "参数" +// @Success 200 {string} string "成功" +// @Security ApiKeyAuth +func BindUpdate(ctx *gin.Context) { + account, err := utils.GetAdmin(ctx) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error()) + return + } + req := &proto.UpdateAdvitiseBindReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + if err := advitiseV2Ser.BindUpdate(ctx, req); err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + opLogSer.AddOpLog(ctx, account, true, req) + stdLog.ServerJSON(ctx, stdLog.Success, "") +} + +// @Tags 广告管理V2/绑定 +// @Summary 删除绑定 +// @Router /api/stat/advitise/v2/bind/del [POST] +// @Param param body proto.DelAdvitiseBindReq true "参数" +// @Success 200 {string} string "成功" +// @Security ApiKeyAuth +func BindDel(ctx *gin.Context) { + account, err := utils.GetAdmin(ctx) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error()) + return + } + req := &proto.DelAdvitiseBindReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + if err := advitiseV2Ser.BindDel(ctx, req.Id); err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + opLogSer.AddOpLog(ctx, account, true, req) + stdLog.ServerJSON(ctx, stdLog.Success, "") +} diff --git a/api/advitiseV2Api/get_click.go b/api/advitiseV2Api/get_click.go new file mode 100644 index 0000000..41df51d --- /dev/null +++ b/api/advitiseV2Api/get_click.go @@ -0,0 +1,49 @@ +package advitiseV2Api + +import ( + "stat/api/proto" + "stat/common/stdLog" + "stat/service/advitiseV2Ser" + + "github.com/gin-gonic/gin" +) + +// @Tags 广告管理V2/外部 +// @Summary 拉广告列表(客户端) +// @Description 按 (appId, locationId?) 返回可展示广告;结构对齐老 /ads/get(advertiseList);每条 item 多带 bindId 供点击回传 +// @Router /api/stat/advitise/v2/get [POST] +// @Param param body proto.GetAdvitiseV2Req true "参数" +// @Success 200 {object} proto.AdvertiseRes "成功" +func GetAdvitises(ctx *gin.Context) { + req := &proto.GetAdvitiseV2Req{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + resp, err := advitiseV2Ser.GetAdvitises(ctx, req) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + // 对齐老 GetAds 直接 ctx.JSON(200, data) 的行为 + ctx.JSON(200, resp) +} + +// @Tags 广告管理V2/外部 +// @Summary 广告点击(客户端) +// @Description 根据 GetAds 返回的 bindId 上报点击,服务端反查 (广告, 位置) 并沿用老统计逻辑(redis 时段 hash + doc 累计) +// @Router /api/stat/advitise/v2/click [POST] +// @Param param body proto.ClickAdvitiseV2Req true "参数" +// @Success 200 {string} string "成功" +func ClickAdvitise(ctx *gin.Context) { + req := &proto.ClickAdvitiseV2Req{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + if err := advitiseV2Ser.ClickAdvitise(ctx, req); err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + stdLog.ServerJSON(ctx, stdLog.Success, "") +} diff --git a/api/advitiseV2Api/info.go b/api/advitiseV2Api/info.go new file mode 100644 index 0000000..7636f08 --- /dev/null +++ b/api/advitiseV2Api/info.go @@ -0,0 +1,106 @@ +package advitiseV2Api + +import ( + "stat/api/proto" + "stat/common/stdLog" + "stat/common/utils" + "stat/service/advitiseV2Ser" + "stat/service/opLogSer" + + "github.com/gin-gonic/gin" +) + +// @Tags 广告管理V2/素材 +// @Summary 素材列表 +// @Router /api/stat/advitise/v2/info/list [POST] +// @Param param body proto.AdvitiseInfoListReq true "参数" +// @Success 200 {object} proto.AdvitiseInfoListResp "成功" +// @Security ApiKeyAuth +func InfoList(ctx *gin.Context) { + req := &proto.AdvitiseInfoListReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + resp, err := advitiseV2Ser.InfoList(ctx, req) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + stdLog.ServerJSON(ctx, stdLog.Success, resp) +} + +// @Tags 广告管理V2/素材 +// @Summary 新增素材 +// @Router /api/stat/advitise/v2/info/add [POST] +// @Param param body proto.AddAdvitiseInfoReq true "参数" +// @Success 200 {string} string "成功" +// @Security ApiKeyAuth +func InfoAdd(ctx *gin.Context) { + account, err := utils.GetAdmin(ctx) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error()) + return + } + req := &proto.AddAdvitiseInfoReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + if err := advitiseV2Ser.InfoAdd(ctx, req); err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + opLogSer.AddOpLog(ctx, account, true, req) + stdLog.ServerJSON(ctx, stdLog.Success, "") +} + +// @Tags 广告管理V2/素材 +// @Summary 更新素材 +// @Router /api/stat/advitise/v2/info/update [POST] +// @Param param body proto.UpdateAdvitiseInfoReq true "参数" +// @Success 200 {string} string "成功" +// @Security ApiKeyAuth +func InfoUpdate(ctx *gin.Context) { + account, err := utils.GetAdmin(ctx) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error()) + return + } + req := &proto.UpdateAdvitiseInfoReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + if err := advitiseV2Ser.InfoUpdate(ctx, req); err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + opLogSer.AddOpLog(ctx, account, true, req) + stdLog.ServerJSON(ctx, stdLog.Success, "") +} + +// @Tags 广告管理V2/素材 +// @Summary 删除素材 +// @Router /api/stat/advitise/v2/info/del [POST] +// @Param param body proto.DelAdvitiseInfoReq true "参数" +// @Success 200 {string} string "成功" +// @Security ApiKeyAuth +func InfoDel(ctx *gin.Context) { + account, err := utils.GetAdmin(ctx) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error()) + return + } + req := &proto.DelAdvitiseInfoReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + if err := advitiseV2Ser.InfoDel(ctx, req.Id); err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + opLogSer.AddOpLog(ctx, account, true, req) + stdLog.ServerJSON(ctx, stdLog.Success, "") +} diff --git a/api/advitiseV2Api/location.go b/api/advitiseV2Api/location.go new file mode 100644 index 0000000..f5230f6 --- /dev/null +++ b/api/advitiseV2Api/location.go @@ -0,0 +1,106 @@ +package advitiseV2Api + +import ( + "stat/api/proto" + "stat/common/stdLog" + "stat/common/utils" + "stat/service/advitiseV2Ser" + "stat/service/opLogSer" + + "github.com/gin-gonic/gin" +) + +// @Tags 广告管理V2/位置 +// @Summary 广告位列表 +// @Router /api/stat/advitise/v2/location/list [POST] +// @Param param body proto.AdvitiseLocationListReq true "参数" +// @Success 200 {object} proto.AdvitiseLocationListResp "成功" +// @Security ApiKeyAuth +func LocationList(ctx *gin.Context) { + req := &proto.AdvitiseLocationListReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + resp, err := advitiseV2Ser.LocationList(ctx, req) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + stdLog.ServerJSON(ctx, stdLog.Success, resp) +} + +// @Tags 广告管理V2/位置 +// @Summary 新增广告位 +// @Router /api/stat/advitise/v2/location/add [POST] +// @Param param body proto.AddAdvitiseLocationReq true "参数" +// @Success 200 {string} string "成功" +// @Security ApiKeyAuth +func LocationAdd(ctx *gin.Context) { + account, err := utils.GetAdmin(ctx) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error()) + return + } + req := &proto.AddAdvitiseLocationReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + if err := advitiseV2Ser.LocationAdd(ctx, req); err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + opLogSer.AddOpLog(ctx, account, true, req) + stdLog.ServerJSON(ctx, stdLog.Success, "") +} + +// @Tags 广告管理V2/位置 +// @Summary 更新广告位 +// @Router /api/stat/advitise/v2/location/update [POST] +// @Param param body proto.UpdateAdvitiseLocationReq true "参数" +// @Success 200 {string} string "成功" +// @Security ApiKeyAuth +func LocationUpdate(ctx *gin.Context) { + account, err := utils.GetAdmin(ctx) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error()) + return + } + req := &proto.UpdateAdvitiseLocationReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + if err := advitiseV2Ser.LocationUpdate(ctx, req); err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + opLogSer.AddOpLog(ctx, account, true, req) + stdLog.ServerJSON(ctx, stdLog.Success, "") +} + +// @Tags 广告管理V2/位置 +// @Summary 删除广告位 +// @Router /api/stat/advitise/v2/location/del [POST] +// @Param param body proto.DelAdvitiseLocationReq true "参数" +// @Success 200 {string} string "成功" +// @Security ApiKeyAuth +func LocationDel(ctx *gin.Context) { + account, err := utils.GetAdmin(ctx) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error()) + return + } + req := &proto.DelAdvitiseLocationReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + if err := advitiseV2Ser.LocationDel(ctx, req.Id); err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + opLogSer.AddOpLog(ctx, account, true, req) + stdLog.ServerJSON(ctx, stdLog.Success, "") +} diff --git a/api/advitiseV2Api/stat.go b/api/advitiseV2Api/stat.go new file mode 100644 index 0000000..6f8b48e --- /dev/null +++ b/api/advitiseV2Api/stat.go @@ -0,0 +1,80 @@ +package advitiseV2Api + +import ( + "stat/api/proto" + "stat/common/stdLog" + "stat/service/advitiseV2Ser" + + "github.com/gin-gonic/gin" +) + +// @Tags 广告管理V2/统计 +// @Summary 广告每日点击列表(汇总) +// @Router /api/stat/advitise/v2/day/click/list [POST] +// @Param param body proto.AdvitiseDayClickListReq true "参数" +// @Success 200 {object} proto.AdvitiseDayClickListResp "成功" +// @Security ApiKeyAuth +func DayClickList(ctx *gin.Context) { + req := &proto.AdvitiseDayClickListReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + resp, err := advitiseV2Ser.DayClickList(ctx, req) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + stdLog.ServerJSON(ctx, stdLog.Success, resp) +} + +// @Tags 广告管理V2/统计 +// @Summary 广告点击详情(细维度) +// @Router /api/stat/advitise/v2/day/click/info [POST] +// @Param param body proto.AdvitiseClickListReq true "参数" +// @Success 200 {object} proto.AdvitiseClickListResp "成功" +// @Security ApiKeyAuth +func DayClickInfo(ctx *gin.Context) { + req := &proto.AdvitiseClickListReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + resp, err := advitiseV2Ser.ClickList(ctx, req) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + stdLog.ServerJSON(ctx, stdLog.Success, resp) +} + +// @Tags 广告管理V2/统计 +// @Summary 广告细维度点击列表(广告位查看统计入口) +// @Router /api/stat/advitise/v2/click/list [POST] +// @Param param body proto.AdvitiseClickListReq true "参数" +// @Success 200 {object} proto.AdvitiseClickListResp "成功" +// @Security ApiKeyAuth +func ClickList(ctx *gin.Context) { + // 与 DayClickInfo 语义一致,复用同一入口 + DayClickInfo(ctx) +} + +// @Tags 广告管理V2/统计 +// @Summary 按时间范围汇总广告点击量(每广告一行) +// @Router /api/stat/advitise/v2/day/click/summary [POST] +// @Param param body proto.AdvitiseClickSummaryReq true "参数" +// @Success 200 {object} proto.AdvitiseClickSummaryResp "成功" +// @Security ApiKeyAuth +func ClickSummary(ctx *gin.Context) { + req := &proto.AdvitiseClickSummaryReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error()) + return + } + resp, err := advitiseV2Ser.ClickSummary(ctx, req) + if err != nil { + stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) + return + } + stdLog.ServerJSON(ctx, stdLog.Success, resp) +} diff --git a/api/fileApi/file.go b/api/fileApi/file.go index a3d5ade..dbe529f 100644 --- a/api/fileApi/file.go +++ b/api/fileApi/file.go @@ -25,9 +25,10 @@ func FileUpload(ctx *gin.Context) { return } // 上传到桶2 - data, err := file.PutObject(ctx, statg.Conf.ProxyUrl.FileRes, &file.FileObject{ + data, err := file.PutObject(ctx, statg.Conf.ProxyUrl.FileRes1, &file.FileObject{ File: f, }) + if err != nil { stdLog.ServerJSON(ctx, stdLog.Failure, err.Error()) } diff --git a/api/proto/advertise.go b/api/proto/advertise.go index 30806e0..287273f 100644 --- a/api/proto/advertise.go +++ b/api/proto/advertise.go @@ -107,21 +107,23 @@ type AdvertiseRes struct { // 外部广告信息 type AdvertiseInfo struct { - Id int64 `json:"id"` // 广告id - Title string `json:"title"` // 广告标题 - CoverImg string `json:"coverImg"` // 封面 - LocId int32 `json:"locId"` // 位置id - JumpType advertiseMod.JumpType `json:"jumpType"` // 跳转方式 0:外部浏览器跳转 1:内部浏览器跳转 2:app内部跳转 - Link string `json:"link"` // 链接地址 - Sort int64 `json:"sort"` // 排序 - Tags string `json:"tags"` // 标签 - Remark string `json:"remark"` // 备注描述 - AmbientColor string `json:"ambientColor"` // 氛围色(逼哩漫画) - Type int // 0:系统 1:渠道 + Id int64 `json:"id"` // 广告id(V2 = advitiseId) + Title string `json:"title"` // 广告标题 + CoverImg string `json:"coverImg"` // 封面 + LocId int32 `json:"locId"` // 位置id + JumpType advertiseMod.JumpType `json:"jumpType"` // 跳转方式 0:外部浏览器跳转 1:内部浏览器跳转 2:app内部跳转 + Link string `json:"link"` // 链接地址 + Sort int64 `json:"sort"` // 排序 + Tags string `json:"tags"` // 标签 + Remark string `json:"remark"` // 备注描述 + AmbientColor string `json:"ambientColor"` // 氛围色(逼哩漫画) + Type int // 0:系统 1:渠道 + BindId int64 `json:"bindId,omitempty"` // V2 点击回传凭证(老接口不填,omitempty 隐藏) } // 广告点击请求 type AdsClickReq struct { - Id *int64 `from:"id" json:"id" binding:"required"` // 广告id + Id *int64 `from:"id" json:"id" binding:"required"` // 广告id(客户端上报,可能已过期) AppId *int32 `from:"appId" json:"appId" binding:"required"` // 应用id + LocId *int32 `from:"locId" json:"locId"` // 位置id(可选;传入后,Id 失效时会按 AppId+LocId 定位最新广告) } diff --git a/api/proto/advitiseV2.go b/api/proto/advitiseV2.go new file mode 100644 index 0000000..4a92aa3 --- /dev/null +++ b/api/proto/advitiseV2.go @@ -0,0 +1,232 @@ +package proto + +import ( + "stat/models/advitiseBindMod" + "stat/models/advitiseInfoMod" + "stat/models/advitiseLocationMod" + + "time" +) + +// ============ 广告素材(Info) ============ + +type AdvitiseInfoListReq struct { + Title string `json:"title" form:"title"` + Remark string `json:"remark" form:"remark"` + Status *bool `json:"status" form:"status"` + PageNum int64 `json:"pageNum" form:"pageNum" binding:"required,gte=1"` + PageSize int64 `json:"pageSize" form:"pageSize" binding:"required,gte=1,lte=500"` +} + +type AdvitiseInfoListResp struct { + Total int64 `json:"total"` + List advitiseInfoMod.AdvitiseInfoList `json:"list"` +} + +type AddAdvitiseInfoReq struct { + Title string `json:"title" binding:"required"` + FullScreenImgs []string `json:"fullScreenImgs"` + Banner720300Imgs []string `json:"banner720300Imgs"` + Banner720150Imgs []string `json:"banner720150Imgs"` + Banner750700Imgs []string `json:"banner750700Imgs"` + Banner800450Imgs []string `json:"banner800450Imgs"` + PopUps600800Imgs []string `json:"popUps600800Imgs"` + PopUps512512Imgs []string `json:"popUps512512Imgs"` + SmallCubeImgs []string `json:"smallCubeImgs"` + VerticalCubeImgs []string `json:"verticalCubeImgs"` + RandomNames []string `json:"randomNames"` + Link string `json:"link"` + Status bool `json:"status"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Remark string `json:"remark"` + Desc string `json:"desc"` +} + +type UpdateAdvitiseInfoReq struct { + Id int64 `json:"id" binding:"required"` + Title *string `json:"title"` + FullScreenImgs *[]string `json:"fullScreenImgs"` + Banner720300Imgs *[]string `json:"banner720300Imgs"` + Banner720150Imgs *[]string `json:"banner720150Imgs"` + Banner750700Imgs *[]string `json:"banner750700Imgs"` + Banner800450Imgs *[]string `json:"banner800450Imgs"` + PopUps600800Imgs *[]string `json:"popUps600800Imgs"` + PopUps512512Imgs *[]string `json:"popUps512512Imgs"` + SmallCubeImgs *[]string `json:"smallCubeImgs"` + VerticalCubeImgs *[]string `json:"verticalCubeImgs"` + RandomNames *[]string `json:"randomNames"` + Link *string `json:"link"` + Status *bool `json:"status"` + StartTime *time.Time `json:"startTime"` + EndTime *time.Time `json:"endTime"` + Remark *string `json:"remark"` + Desc *string `json:"desc"` +} + +type DelAdvitiseInfoReq struct { + Id int64 `json:"id" binding:"required"` +} + +// ============ 广告位(Location) ============ + +type AdvitiseLocationListReq struct { + AppId *int32 `json:"appId" form:"appId"` + LocId *int32 `json:"locId" form:"locId"` + Name string `json:"name" form:"name"` + PageNum int64 `json:"pageNum" form:"pageNum" binding:"required,gte=1"` + PageSize int64 `json:"pageSize" form:"pageSize" binding:"required,gte=1,lte=500"` +} + +type AdvitiseLocationListResp struct { + Total int64 `json:"total"` + List advitiseLocationMod.AdvitiseLocationList `json:"list"` +} + +type AddAdvitiseLocationReq struct { + AppId int32 `json:"appId" binding:"required"` + LocId int32 `json:"locId" binding:"required"` + Name string `json:"name" binding:"required"` + AdvitiseSize advitiseLocationMod.AdvitiseSizeType `json:"advitiseSize"` + ImgSizeDesc string `json:"imgSizeDesc"` + Remark string `json:"remark"` +} + +type UpdateAdvitiseLocationReq struct { + Id int64 `json:"id" binding:"required"` + AppId *int32 `json:"appId"` + LocId *int32 `json:"locId"` + Name *string `json:"name"` + AdvitiseSize *advitiseLocationMod.AdvitiseSizeType `json:"advitiseSize"` + ImgSizeDesc *string `json:"imgSizeDesc"` + Remark *string `json:"remark"` +} + +type DelAdvitiseLocationReq struct { + Id int64 `json:"id" binding:"required"` +} + +// ============ 广告绑定(Bind) ============ + +type AdvitiseBindListReq struct { + AppId *int32 `json:"appId" form:"appId"` + AdvitiseId *int64 `json:"advitiseId" form:"advitiseId"` + LocationId *int32 `json:"locationId" form:"locationId"` + PageNum int64 `json:"pageNum" form:"pageNum" binding:"required,gte=1"` + PageSize int64 `json:"pageSize" form:"pageSize" binding:"required,gte=1,lte=500"` +} + +type AdvitiseBindListResp struct { + Total int64 `json:"total"` + List advitiseBindMod.AdvitiseBindList `json:"list"` +} + +type AddAdvitiseBindReq struct { + AppId int32 `json:"appId" binding:"required"` + AdvitiseId int64 `json:"advitiseId" binding:"required"` + LocationId []int32 `json:"locationId" binding:"required"` + Sort int64 `json:"sort"` +} + +type UpdateAdvitiseBindReq struct { + Id int64 `json:"id" binding:"required"` + AppId *int32 `json:"appId"` + AdvitiseId *int64 `json:"advitiseId"` + LocationId *[]int32 `json:"locationId"` + Sort *int64 `json:"sort"` +} + +type DelAdvitiseBindReq struct { + Id int64 `json:"id" binding:"required"` +} + +// ============ 外部获取广告 / 点击 ============ + +// GetAdvitiseV2Req 客户端拉广告列表 +type GetAdvitiseV2Req struct { + AppId int32 `json:"appId" binding:"required"` + LocationId *int32 `json:"locationId"` // 可选,指定位置 +} + +// 响应结构直接复用老 proto.AdvertiseRes (advertiseList) — V2 客户端从 item.bindId 拿点击回传凭证 + +// ClickAdvitiseV2Req 客户端点击回传 +// LocationId 必须传:细维度统计表(advitise_click_stat)按 (appId, advitiseId, locationId, day) 聚合 +type ClickAdvitiseV2Req struct { + Id int64 `from:"id" json:"id" binding:"required"` // 广告id + // LocationId int32 `json:"locationId" binding:"required"` // 客户端展示时选中的位置 +} + +// ============ 统计管理 ============ + +// AdvitiseDayClickListReq 每日点击列表(汇总,来自 advitise_day_click_stat) +type AdvitiseDayClickListReq struct { + Id *int64 `json:"id" form:"id"` // 广告id(advitiseId) + StatStartDate string `json:"statStartDate" form:"statStartDate"` // yyyy-MM-dd HH:mm:ss + StatEndDate string `json:"statEndDate" form:"statEndDate"` + PageNum int64 `json:"pageNum" form:"pageNum" binding:"required,gte=1"` + PageSize int64 `json:"pageSize" form:"pageSize" binding:"required,gte=1,lte=500"` +} + +type AdvitiseDayClickItem struct { + AdsId int64 `json:"adsId"` // 广告id (advitiseId) + AdsName string `json:"adsName"` // 广告标题 + Clicks int64 `json:"clicks"` + StatDate string `json:"statDate"` // yyyy-MM-dd +} + +type AdvitiseDayClickListResp struct { + Total int64 `json:"total"` + List []*AdvitiseDayClickItem `json:"list"` +} + +// AdvitiseClickListReq 细维度列表(来自 advitise_click_stat) +type AdvitiseClickListReq struct { + AdsId *int64 `json:"adsId" form:"adsId"` // 广告id(advitiseId) + AppId *int32 `json:"appId" form:"appId"` + LocationId *int32 `json:"locationId" form:"locationId"` + StatStartDate string `json:"statStartDate" form:"statStartDate"` + StatEndDate string `json:"statEndDate" form:"statEndDate"` + PageNum int64 `json:"pageNum" form:"pageNum" binding:"required,gte=1"` + PageSize int64 `json:"pageSize" form:"pageSize" binding:"required,gte=1,lte=500"` +} + +type AdvitiseClickItem struct { + AppId int32 `json:"appId"` + AppName string `json:"appName"` + AdsId int64 `json:"adsId"` + AdsName string `json:"adsName"` + LocationId int32 `json:"locationId"` + LocationName string `json:"locationName"` + Clicks int64 `json:"clicks"` + StatDate string `json:"statDate"` +} + +type AdvitiseClickListResp struct { + Total int64 `json:"total"` + List []*AdvitiseClickItem `json:"list"` +} + +// AdvitiseClickSummaryReq 按时间范围汇总广告点击量,每个广告一行 +// StatStartDate/StatEndDate 至少填一个;可选 id/appId/locationId 缩窄范围 +type AdvitiseClickSummaryReq struct { + Id *int64 `json:"id" form:"id"` + AppId *int32 `json:"appId" form:"appId"` + LocationId *int32 `json:"locationId" form:"locationId"` + StatStartDate string `json:"statStartDate" form:"statStartDate"` + StatEndDate string `json:"statEndDate" form:"statEndDate"` + PageNum int64 `json:"pageNum" form:"pageNum" binding:"required,gte=1"` + PageSize int64 `json:"pageSize" form:"pageSize" binding:"required,gte=1,lte=500"` +} + +type AdvitiseClickSummaryItem struct { + AdsId int64 `json:"adsId"` + AdsName string `json:"adsName"` + TotalClicks int64 `json:"totalClicks"` +} + +type AdvitiseClickSummaryResp struct { + Total int64 `json:"total"` // 匹配到的广告数 + GrandTotal int64 `json:"grandTotal"` // 时间范围内所有点击总量 + List []*AdvitiseClickSummaryItem `json:"list"` +} diff --git a/api/versionApi/cpcdownload.go b/api/versionApi/cpcdownload.go index b43751a..762173d 100644 --- a/api/versionApi/cpcdownload.go +++ b/api/versionApi/cpcdownload.go @@ -22,7 +22,7 @@ import ( // @Success 200 {file} file "ios返回mobileconfig文件" // @Success 302 {string} string "android跳转下载地址" // @Failure 400 {string} string "失败" -// @Router /rsdownload/{appId}/{channel} [get] +// @Router /download/{appId}/{channel} [get] func CpcDownload(ctx *gin.Context) { // 第一个参数 appId appId, err := strconv.Atoi(ctx.Param("appId")) diff --git a/common/genId/lid.go b/common/genId/lid.go index 006c0e1..51ac258 100644 --- a/common/genId/lid.go +++ b/common/genId/lid.go @@ -20,7 +20,10 @@ var audioBook *lid.Lid // 有声小说自增器 var landpageConfig *lid.Lid // 落地页配置id var appCategory *lid.Lid // var appLocLid *lid.Lid // 应用中心位置id自增器 -var cpcDownloadCfgLid *lid.Lid // CPC下载配置id自增器 +var cpcDownloadCfgLid *lid.Lid // CPC下载配置id自增器 +var advitiseInfoLid *lid.Lid // 广告素材(V2) id自增器 +var advitiseLocationLid *lid.Lid // 广告位(V2) id自增器 +var advitiseBindLid *lid.Lid // 广告绑定(V2) id自增器 func Start() { var err error @@ -104,6 +107,21 @@ func Start() { if err != nil { panic(err) } + + advitiseInfoLid, err = lid.NewLid(collection, "advitise_info_id", 100) + if err != nil { + panic(err) + } + + advitiseLocationLid, err = lid.NewLid(collection, "advitise_location_id", 100) + if err != nil { + panic(err) + } + + advitiseBindLid, err = lid.NewLid(collection, "advitise_bind_id", 100) + if err != nil { + panic(err) + } } func GetNextRoleId() int64 { @@ -169,3 +187,15 @@ func GetNextAppLocId() int64 { func GetCpcDownloadCfgId() int64 { return cpcDownloadCfgLid.Get() } + +func GetAdvitiseInfoId() int64 { + return advitiseInfoLid.Get() +} + +func GetAdvitiseLocationId() int64 { + return advitiseLocationLid.Get() +} + +func GetAdvitiseBindId() int64 { + return advitiseBindLid.Get() +} diff --git a/conf/91fans.mobileconfig b/conf/91fans.mobileconfig new file mode 100644 index 0000000..956ae8e --- /dev/null +++ b/conf/91fans.mobileconfig @@ -0,0 +1,62 @@ + + + + + ConsentText + + default + 91fans + + PayloadContent + + + FullScreen + + Icon + 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 + IgnoreManifestScope + + IsRemovable + + Label + 91fans + PayloadDescription + 配置 Web Clip 设置 + PayloadDisplayName + Web Clip + PayloadIdentifier + com.apple.webClip.managed.7655CF41-CA28-4ADA-A9C8-38BB0A7C3B7E + PayloadType + com.apple.webClip.managed + PayloadUUID + 7655CF41-CA28-4ADA-A9C8-38BB0A7C3B7E + PayloadVersion + 1 + Precomposed + + URL + https://d2l4gspie962rq.cloudfront.net/#/ + + + PayloadDescription + 请点击右上角「安装」按钮 +为了避免大家无法观影,请安装iOS轻量版,保障您的观影权益!该安装证书已通过苹果官方认证,安全可靠。 +由于苹果商店版审核政策原因,导致用户无法愉快的观影,该安装仅仅是在您的手机桌面增加一个平台入口,请放心安装使用。 +如果您的手机设置了锁屏密码,安装时需要输入密码后才能继续安装。 +感谢您对麻豆官方的支持,祝您观影愉快! + PayloadDisplayName + 91fans + PayloadIdentifier + luli.C10D036B-C38C-4830-9767-D68130AEFA78 + PayloadOrganization + 91fans + PayloadRemovalDisallowed + + PayloadType + Configuration + PayloadUUID + FBB5F556-C0EF-4DBC-AFB5-558AA3662493 + PayloadVersion + 1 + + \ No newline at end of file diff --git a/conf/huangguo.mobileconfig b/conf/huangguo.mobileconfig new file mode 100644 index 0000000..09b0cc6 --- /dev/null +++ b/conf/huangguo.mobileconfig @@ -0,0 +1,62 @@ + + + + + ConsentText + + default + 黄果短剧 + + PayloadContent + + + FullScreen + + Icon + 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 + IgnoreManifestScope + + IsRemovable + + Label + 黄果短剧 + PayloadDescription + 配置 Web Clip 设置 + PayloadDisplayName + Web Clip + PayloadIdentifier + com.apple.webClip.managed.7655CF41-CA28-4ADA-A9C8-38BB0A7C3B7E + PayloadType + com.apple.webClip.managed + PayloadUUID + 7655CF41-CA28-4ADA-A9C8-38BB0A7C3B7E + PayloadVersion + 1 + Precomposed + + URL + https://d2l4gspie962rq.cloudfront.net/#/ + + + PayloadDescription + 请点击右上角「安装」按钮 +为了避免大家无法观影,请安装iOS轻量版,保障您的观影权益!该安装证书已通过苹果官方认证,安全可靠。 +由于苹果商店版审核政策原因,导致用户无法愉快的观影,该安装仅仅是在您的手机桌面增加一个平台入口,请放心安装使用。 +如果您的手机设置了锁屏密码,安装时需要输入密码后才能继续安装。 +感谢您对麻豆官方的支持,祝您观影愉快! + PayloadDisplayName + 黄果短剧 + PayloadIdentifier + luli.C10D036B-C38C-4830-9767-D68130AEFA78 + PayloadOrganization + 黄果短剧 + PayloadRemovalDisallowed + + PayloadType + Configuration + PayloadUUID + FBB5F556-C0EF-4DBC-AFB5-558AA3662493 + PayloadVersion + 1 + + \ No newline at end of file diff --git a/deploy/nginx/cpcdownload.mfcg.me.conf b/deploy/nginx/cpcdownload.mfcg.me.conf new file mode 100644 index 0000000..73e95e7 --- /dev/null +++ b/deploy/nginx/cpcdownload.mfcg.me.conf @@ -0,0 +1,58 @@ +# /etc/nginx/conf.d/cpcdownload.mfcg.me.conf +# +# cpcdownload.mfcg.me -> stat_center Go 服务 (127.0.0.1:8089) +# 用途: +# - GET /download/{appId}/{channel} 双端下载落地页(ios 返回 mobileconfig / android 302 跳转) +# - POST /api/stat/cpc/download/{list,add,update,del} 后台 CPC 下载配置管理 +# +# 部署: +# scp deploy/nginx/cpcdownload.mfcg.me.conf root@:/etc/nginx/conf.d/ +# ssh root@ 'nginx -t && nginx -s reload' + +# HTTP -> HTTPS +server { + listen 80; + listen [::]:80; + server_name cpcdownload.mfcg.me; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name cpcdownload.mfcg.me; + + # 证书路径按你机器上的实际位置调整(沿用同 mfcg.me 通配证书即可) + ssl_certificate /etc/nginx/ssl/mfcg.me/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/mfcg.me/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + access_log /var/log/nginx/cpcdownload.mfcg.me.access.log; + error_log /var/log/nginx/cpcdownload.mfcg.me.error.log; + + # mobileconfig 大小通常很小,这里放宽到 8M 兜底 + client_max_body_size 8m; + + location / { + proxy_pass http://127.0.0.1:8089; + proxy_http_version 1.1; + + # 透传真实 IP(后端 ctx.ClientIP() 依赖此头取 IP 入库 landingpage_cpc) + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # mobileconfig 是二进制,保持原样;不做缓冲以便 302 立即下发 + proxy_buffering off; + proxy_request_buffering off; + + proxy_connect_timeout 5s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } +} diff --git a/middleware/auth/http.go b/middleware/auth/http.go index 7fda456..222ef39 100644 --- a/middleware/auth/http.go +++ b/middleware/auth/http.go @@ -86,7 +86,9 @@ var ( "/api/stat/daily-stat/hour", "/api/stat/daily-stat/rank", "/api/stat/daily-stat/total", - "/rsdownload/", + "/download/", + "/api/stat/advitise/v2/get", + "/api/stat/advitise/v2/click", } ) diff --git a/models/advertiseMod/advertise.go b/models/advertiseMod/advertise.go index cf91404..eea36b9 100644 --- a/models/advertiseMod/advertise.go +++ b/models/advertiseMod/advertise.go @@ -150,6 +150,30 @@ func FindAdsById(ctx context.Context, id int64) (data Advertise, err error) { return } +// FindLatestAdsByAppAndLoc 取该应用/位置当前生效的最新一条广告(用于点击回退到最新广告) +// locId <= 0 表示不按位置过滤,只在应用维度取最新 +func FindLatestAdsByAppAndLoc(ctx context.Context, appId, locId int32) (data Advertise, err error) { + now := time.Now() + filter := bson.M{ + "appId": appId, + "isActive": true, + "startTime": bson.M{"$lte": now}, + "endTime": bson.M{"$gte": now}, + } + if locId > 0 { + filter["locId"] = locId + } + opts := options.FindOne().SetSort(bson.D{ + {Key: "sort", Value: 1}, + {Key: "createAt", Value: -1}, + }) + if err = db.FindOne(ctx, &data, filter, opts); err != nil { + log.Error("advertise FindLatestAdsByAppAndLoc FindOne err", log.E(err)) + return + } + return +} + // 增加广告点击量 func IncAdsClickCount(ctx context.Context, id int64) (err error) { filter := bson.M{"_id": id} diff --git a/models/advitiseBindMod/bind.go b/models/advitiseBindMod/bind.go new file mode 100644 index 0000000..a544816 --- /dev/null +++ b/models/advitiseBindMod/bind.go @@ -0,0 +1,78 @@ +package advitiseBindMod + +import ( + "context" + "fmt" + "stat/common/log" + "stat/common/mongos" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func Init(ctx context.Context) { + db = mongos.New(mongos.MgoManager, TN.AdvitiseBind) + indexes := []mongo.IndexModel{ + {Keys: bson.D{ + {Key: FN.AdvitiseBind.AppId, Value: 1}, + {Key: FN.AdvitiseBind.LocationId, Value: 1}, + }}, + {Keys: bson.D{{Key: FN.AdvitiseBind.AdvitiseId, Value: 1}}}, + {Keys: bson.D{{Key: FN.AdvitiseBind.CreatedAt, Value: -1}}}, + } + if _, err := db.CreateIndex(ctx, indexes); err != nil { + panic(fmt.Sprintf("(advitiseBindMod:Init) CreateIndex failed: %+v", err)) + } +} + +func InsertOneAdvitiseBind(ctx context.Context, doc *AdvitiseBind) error { + if _, err := db.InsertOne(ctx, doc); err != nil { + log.Warn("advitiseBindMod InsertOne failed", log.E(err)) + return err + } + return nil +} + +func FindOneAdvitiseBind(ctx context.Context, filter bson.M) (*AdvitiseBind, error) { + var doc AdvitiseBind + if err := db.FindOne(ctx, &doc, filter); err != nil { + return nil, err + } + return &doc, nil +} + +func DelOneAdvitiseBind(ctx context.Context, filter bson.M) (int64, error) { + count, err := db.DeleteOne(ctx, filter) + if err != nil { + log.Warn("advitiseBindMod DeleteOne failed", log.E(err), log.Any("filter", filter)) + return 0, err + } + return count, nil +} + +func UpdateOneAdvitiseBind(ctx context.Context, filter bson.M, update bson.M) error { + if _, err := db.UpdateOne(ctx, filter, update); err != nil { + log.Warn("advitiseBindMod UpdateOne failed", log.E(err), log.Any("filter", filter)) + return err + } + return nil +} + +func CountAdvitiseBinds(ctx context.Context, filter bson.M) (int64, error) { + count, err := db.Count(ctx, filter) + if err != nil { + log.Warn("advitiseBindMod Count failed", log.E(err), log.Any("filter", filter)) + return 0, err + } + return count, nil +} + +func FindManyAdvitiseBinds(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (AdvitiseBindList, error) { + list := make(AdvitiseBindList, 0) + if err := db.Find(ctx, &list, filter, opts...); err != nil { + log.Warn("advitiseBindMod Find failed", log.E(err), log.Any("filter", filter)) + return nil, err + } + return list, nil +} diff --git a/models/advitiseBindMod/tag.go b/models/advitiseBindMod/tag.go new file mode 100644 index 0000000..eb113d2 --- /dev/null +++ b/models/advitiseBindMod/tag.go @@ -0,0 +1,30 @@ +package advitiseBindMod + +type TagAdvitiseBind struct { + Id string + AppId string + AdvitiseId string + LocationId string + Sort string + CreatedAt string + UpdatedAt string +} + +var FN struct { + AdvitiseBind TagAdvitiseBind +} + +var TN struct { + AdvitiseBind string +} + +func init() { + FN.AdvitiseBind.Id = "_id" + FN.AdvitiseBind.AppId = "appId" + FN.AdvitiseBind.AdvitiseId = "advitiseId" + FN.AdvitiseBind.LocationId = "locationId" + FN.AdvitiseBind.Sort = "sort" + FN.AdvitiseBind.CreatedAt = "createdAt" + FN.AdvitiseBind.UpdatedAt = "updatedAt" + TN.AdvitiseBind = "advitise_bind" +} diff --git a/models/advitiseBindMod/type.go b/models/advitiseBindMod/type.go new file mode 100644 index 0000000..72c7e48 --- /dev/null +++ b/models/advitiseBindMod/type.go @@ -0,0 +1,22 @@ +package advitiseBindMod + +import ( + "stat/common/mongos" + "time" +) + +var db *mongos.MongoTool + +// AdvitiseBind 广告投放绑定(V2):把一个 AdvitiseInfo 绑到指定 (appId, locationId[]) 上 +// @table:advitise_bind +type AdvitiseBind struct { + Id int64 `json:"id" bson:"_id"` + AppId int32 `json:"appId" bson:"appId"` // 产品id + AdvitiseId int64 `json:"advitiseId" bson:"advitiseId"` // 广告id -> advitiseInfoMod + LocationId []int32 `json:"locationId" bson:"locationId"` // 位置id列表(可多) + Sort int64 `json:"sort" bson:"sort"` // 排序值,优先级 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +type AdvitiseBindList []*AdvitiseBind diff --git a/models/advitiseClickStatMod/stat.go b/models/advitiseClickStatMod/stat.go new file mode 100644 index 0000000..a062bb9 --- /dev/null +++ b/models/advitiseClickStatMod/stat.go @@ -0,0 +1,99 @@ +package advitiseClickStatMod + +import ( + "context" + "fmt" + "stat/common/log" + "stat/common/mongos" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func Init(ctx context.Context) { + db = mongos.New(mongos.MgoManager, TN.AdvitiseClickStat) + indexes := []mongo.IndexModel{ + { + Keys: bson.D{ + {Key: FN.AdvitiseClickStat.AppId, Value: 1}, + {Key: FN.AdvitiseClickStat.AdvitiseId, Value: 1}, + {Key: FN.AdvitiseClickStat.LocationId, Value: 1}, + {Key: FN.AdvitiseClickStat.StatDate, Value: -1}, + }, + }, + {Keys: bson.D{ + {Key: FN.AdvitiseClickStat.AdvitiseId, Value: 1}, + {Key: FN.AdvitiseClickStat.StatDate, Value: -1}, + }}, + {Keys: bson.D{ + {Key: FN.AdvitiseClickStat.StatDate, Value: -1}, + }}, + } + if _, err := db.CreateIndex(ctx, indexes); err != nil { + panic(fmt.Sprintf("(advitiseClickStatMod:Init) CreateIndex failed: %+v", err)) + } +} + +// InsertOrUpdate 按 (appId, advitiseId, locationId, statDate) upsert +1 +// 对齐 media/biadsclickmod.InsertOrUpdateBIAdsClickModel +func InsertOrUpdate(ctx context.Context, appId int32, advitiseId int64, locationId int32, statDate time.Time) error { + filter := bson.M{ + FN.AdvitiseClickStat.AppId: appId, + FN.AdvitiseClickStat.AdvitiseId: advitiseId, + FN.AdvitiseClickStat.LocationId: locationId, + FN.AdvitiseClickStat.StatDate: statDate, + } + update := bson.M{ + "$inc": bson.M{FN.AdvitiseClickStat.Clicks: 1}, + "$setOnInsert": bson.M{ + FN.AdvitiseClickStat.CreatedAt: time.Now(), + }, + } + if _, err := db.UpsertOneEx(ctx, filter, update); err != nil { + log.Warn("advitiseClickStatMod InsertOrUpdate upsert failed", + log.E(err), log.Any("filter", filter)) + return err + } + return nil +} + +func FindMany(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (AdvitiseClickStatList, error) { + list := make(AdvitiseClickStatList, 0) + if err := db.Find(ctx, &list, filter, opts...); err != nil { + log.Warn("advitiseClickStatMod Find failed", log.E(err), log.Any("filter", filter)) + return nil, err + } + return list, nil +} + +func Count(ctx context.Context, filter bson.M) (int64, error) { + c, err := db.Count(ctx, filter) + if err != nil { + log.Warn("advitiseClickStatMod Count failed", log.E(err), log.Any("filter", filter)) + return 0, err + } + return c, nil +} + +// Aggregate 通用聚合(留给上层写 pipeline) +func Aggregate(ctx context.Context, bind interface{}, pipeline []bson.M) error { + return db.Aggregate(ctx, bind, pipeline) +} + +// GetAppClicks 按 appId + statDate 汇总点击(对齐 media/biadsclickmod.GetAppClicks) +func GetAppClicks(ctx context.Context, appId int32, statDate time.Time) (int64, error) { + list, err := FindMany(ctx, bson.M{ + FN.AdvitiseClickStat.AppId: appId, + FN.AdvitiseClickStat.StatDate: statDate, + }) + if err != nil { + return 0, err + } + total := int64(0) + for _, i := range list { + total += i.Clicks + } + return total, nil +} diff --git a/models/advitiseClickStatMod/tag.go b/models/advitiseClickStatMod/tag.go new file mode 100644 index 0000000..cf9ea03 --- /dev/null +++ b/models/advitiseClickStatMod/tag.go @@ -0,0 +1,30 @@ +package advitiseClickStatMod + +type TagAdvitiseClickStat struct { + Id string + AppId string + AdvitiseId string + LocationId string + Clicks string + StatDate string + CreatedAt string +} + +var FN struct { + AdvitiseClickStat TagAdvitiseClickStat +} + +var TN struct { + AdvitiseClickStat string +} + +func init() { + FN.AdvitiseClickStat.Id = "_id" + FN.AdvitiseClickStat.AppId = "appId" + FN.AdvitiseClickStat.AdvitiseId = "advitiseId" + FN.AdvitiseClickStat.LocationId = "locationId" + FN.AdvitiseClickStat.Clicks = "clicks" + FN.AdvitiseClickStat.StatDate = "statDate" + FN.AdvitiseClickStat.CreatedAt = "createdAt" + TN.AdvitiseClickStat = "advitise_click_stat" +} diff --git a/models/advitiseClickStatMod/type.go b/models/advitiseClickStatMod/type.go new file mode 100644 index 0000000..386026c --- /dev/null +++ b/models/advitiseClickStatMod/type.go @@ -0,0 +1,25 @@ +package advitiseClickStatMod + +import ( + "stat/common/mongos" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var db *mongos.MongoTool + +// AdvitiseClickStat 广告点击天粒度细维度统计(对齐 media/biadsclickmod) +// 键: (appId, advitiseId, locationId, statDate) +// @table:advitise_click_stat +type AdvitiseClickStat struct { + Id primitive.ObjectID `json:"id" bson:"_id,omitempty"` + AppId int32 `json:"appId" bson:"appId"` + AdvitiseId int64 `json:"advitiseId" bson:"advitiseId"` + LocationId int32 `json:"locationId" bson:"locationId"` + Clicks int64 `json:"clicks" bson:"clicks"` + StatDate time.Time `json:"statDate" bson:"statDate"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +type AdvitiseClickStatList []*AdvitiseClickStat diff --git a/models/advitiseDayClickStatMod/stat.go b/models/advitiseDayClickStatMod/stat.go new file mode 100644 index 0000000..3b30d58 --- /dev/null +++ b/models/advitiseDayClickStatMod/stat.go @@ -0,0 +1,68 @@ +package advitiseDayClickStatMod + +import ( + "context" + "fmt" + "stat/common/log" + "stat/common/mongos" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func Init(ctx context.Context) { + db = mongos.New(mongos.MgoManager, TN.AdvitiseDayClickStat) + indexes := []mongo.IndexModel{ + {Keys: bson.D{ + {Key: FN.AdvitiseDayClickStat.AdvitiseId, Value: 1}, + {Key: FN.AdvitiseDayClickStat.StatDate, Value: -1}, + }}, + {Keys: bson.D{ + {Key: FN.AdvitiseDayClickStat.StatDate, Value: -1}, + }}, + } + if _, err := db.CreateIndex(ctx, indexes); err != nil { + panic(fmt.Sprintf("(advitiseDayClickStatMod:Init) CreateIndex failed: %+v", err)) + } +} + +// InsertOrUpdate 按 (advitiseId, statDate) upsert +1 +// 对齐 media/adsdayclickstatmod.InsertOrUpdateAdsDayClickStatModel +func InsertOrUpdate(ctx context.Context, advitiseId int64, statDate time.Time) error { + filter := bson.M{ + FN.AdvitiseDayClickStat.AdvitiseId: advitiseId, + FN.AdvitiseDayClickStat.StatDate: statDate, + } + update := bson.M{ + "$inc": bson.M{FN.AdvitiseDayClickStat.Clicks: 1}, + "$setOnInsert": bson.M{ + FN.AdvitiseDayClickStat.CreatedAt: time.Now(), + }, + } + if _, err := db.UpsertOneEx(ctx, filter, update); err != nil { + log.Warn("advitiseDayClickStatMod InsertOrUpdate upsert failed", + log.E(err), log.Any("filter", filter)) + return err + } + return nil +} + +func FindMany(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (AdvitiseDayClickStatList, error) { + list := make(AdvitiseDayClickStatList, 0) + if err := db.Find(ctx, &list, filter, opts...); err != nil { + log.Warn("advitiseDayClickStatMod Find failed", log.E(err), log.Any("filter", filter)) + return nil, err + } + return list, nil +} + +func Count(ctx context.Context, filter bson.M) (int64, error) { + c, err := db.Count(ctx, filter) + if err != nil { + log.Warn("advitiseDayClickStatMod Count failed", log.E(err), log.Any("filter", filter)) + return 0, err + } + return c, nil +} diff --git a/models/advitiseDayClickStatMod/tag.go b/models/advitiseDayClickStatMod/tag.go new file mode 100644 index 0000000..4b0229c --- /dev/null +++ b/models/advitiseDayClickStatMod/tag.go @@ -0,0 +1,26 @@ +package advitiseDayClickStatMod + +type TagAdvitiseDayClickStat struct { + Id string + AdvitiseId string + Clicks string + StatDate string + CreatedAt string +} + +var FN struct { + AdvitiseDayClickStat TagAdvitiseDayClickStat +} + +var TN struct { + AdvitiseDayClickStat string +} + +func init() { + FN.AdvitiseDayClickStat.Id = "_id" + FN.AdvitiseDayClickStat.AdvitiseId = "advitiseId" + FN.AdvitiseDayClickStat.Clicks = "clicks" + FN.AdvitiseDayClickStat.StatDate = "statDate" + FN.AdvitiseDayClickStat.CreatedAt = "createdAt" + TN.AdvitiseDayClickStat = "advitise_day_click_stat" +} diff --git a/models/advitiseDayClickStatMod/type.go b/models/advitiseDayClickStatMod/type.go new file mode 100644 index 0000000..8dcac0c --- /dev/null +++ b/models/advitiseDayClickStatMod/type.go @@ -0,0 +1,23 @@ +package advitiseDayClickStatMod + +import ( + "stat/common/mongos" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var db *mongos.MongoTool + +// AdvitiseDayClickStat 广告点击天粒度汇总(不含位置维度,对齐 media/adsdayclickstatmod) +// 键: (advitiseId, statDate) +// @table:advitise_day_click_stat +type AdvitiseDayClickStat struct { + Id primitive.ObjectID `json:"id" bson:"_id,omitempty"` + AdvitiseId int64 `json:"advitiseId" bson:"advitiseId"` + Clicks int64 `json:"clicks" bson:"clicks"` + StatDate time.Time `json:"statDate" bson:"statDate"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +type AdvitiseDayClickStatList []*AdvitiseDayClickStat diff --git a/models/advitiseInfoMod/advitise.go b/models/advitiseInfoMod/advitise.go new file mode 100644 index 0000000..3a5fcd7 --- /dev/null +++ b/models/advitiseInfoMod/advitise.go @@ -0,0 +1,79 @@ +package advitiseInfoMod + +import ( + "context" + "fmt" + "stat/common/log" + "stat/common/mongos" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func Init(ctx context.Context) { + db = mongos.New(mongos.MgoManager, TN.AdvitiseInfo) + indexes := []mongo.IndexModel{ + {Keys: bson.D{ + {Key: FN.AdvitiseInfo.StartTime, Value: 1}, + {Key: FN.AdvitiseInfo.EndTime, Value: 1}, + {Key: FN.AdvitiseInfo.Status, Value: 1}, + }}, + {Keys: bson.D{{Key: FN.AdvitiseInfo.CreatedAt, Value: -1}}}, + } + if _, err := db.CreateIndex(ctx, indexes); err != nil { + panic(fmt.Sprintf("(advitiseInfoMod:Init) CreateIndex failed: %+v", err)) + } +} + +func InsertOneAdvitiseInfo(ctx context.Context, doc *AdvitiseInfo) error { + if _, err := db.InsertOne(ctx, doc); err != nil { + log.Warn("advitiseInfoMod InsertOne failed", log.E(err)) + return err + } + return nil +} + +func FindOneAdvitiseInfo(ctx context.Context, filter bson.M) (*AdvitiseInfo, error) { + var doc AdvitiseInfo + if err := db.FindOne(ctx, &doc, filter); err != nil { + return nil, err + } + return &doc, nil +} + +func DelOneAdvitiseInfo(ctx context.Context, filter bson.M) (int64, error) { + count, err := db.DeleteOne(ctx, filter) + if err != nil { + log.Warn("advitiseInfoMod DeleteOne failed", log.E(err), log.Any("filter", filter)) + return 0, err + } + return count, nil +} + +func UpdateOneAdvitiseInfo(ctx context.Context, filter bson.M, update bson.M) error { + if _, err := db.UpdateOne(ctx, filter, update); err != nil { + log.Warn("advitiseInfoMod UpdateOne failed", log.E(err), log.Any("filter", filter)) + return err + } + return nil +} + +func CountAdvitiseInfos(ctx context.Context, filter bson.M) (int64, error) { + count, err := db.Count(ctx, filter) + if err != nil { + log.Warn("advitiseInfoMod Count failed", log.E(err), log.Any("filter", filter)) + return 0, err + } + return count, nil +} + +func FindManyAdvitiseInfos(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (AdvitiseInfoList, error) { + list := make(AdvitiseInfoList, 0) + if err := db.Find(ctx, &list, filter, opts...); err != nil { + log.Warn("advitiseInfoMod Find failed", log.E(err), log.Any("filter", filter)) + return nil, err + } + return list, nil +} + diff --git a/models/advitiseInfoMod/tag.go b/models/advitiseInfoMod/tag.go new file mode 100644 index 0000000..edc1931 --- /dev/null +++ b/models/advitiseInfoMod/tag.go @@ -0,0 +1,56 @@ +package advitiseInfoMod + +type TagAdvitiseInfo struct { + Id string + Title string + FullScreenImgs string + Banner720300Imgs string + Banner720150Imgs string + Banner750700Imgs string + Banner800450Imgs string + PopUps600800Imgs string + PopUps512512Imgs string + SmallCubeImgs string + VerticalCubeImgs string + RandomNames string + Link string + Status string + StartTime string + EndTime string + Remark string + Desc string + CreatedAt string + UpdatedAt string +} + +var FN struct { + AdvitiseInfo TagAdvitiseInfo +} + +var TN struct { + AdvitiseInfo string +} + +func init() { + FN.AdvitiseInfo.Id = "_id" + FN.AdvitiseInfo.Title = "title" + FN.AdvitiseInfo.FullScreenImgs = "fullScreenImgs" + FN.AdvitiseInfo.Banner720300Imgs = "banner720300Imgs" + FN.AdvitiseInfo.Banner720150Imgs = "banner720150Imgs" + FN.AdvitiseInfo.Banner750700Imgs = "banner750700Imgs" + FN.AdvitiseInfo.Banner800450Imgs = "banner800450Imgs" + FN.AdvitiseInfo.PopUps600800Imgs = "popUps600800Imgs" + FN.AdvitiseInfo.PopUps512512Imgs = "popUps512512Imgs" + FN.AdvitiseInfo.SmallCubeImgs = "smallCubeImgs" + FN.AdvitiseInfo.VerticalCubeImgs = "verticalCubeImgs" + FN.AdvitiseInfo.RandomNames = "randomNames" + FN.AdvitiseInfo.Link = "link" + FN.AdvitiseInfo.Status = "status" + FN.AdvitiseInfo.StartTime = "startTime" + FN.AdvitiseInfo.EndTime = "endTime" + FN.AdvitiseInfo.Remark = "remark" + FN.AdvitiseInfo.Desc = "desc" + FN.AdvitiseInfo.CreatedAt = "createdAt" + FN.AdvitiseInfo.UpdatedAt = "updatedAt" + TN.AdvitiseInfo = "advitise_info" +} diff --git a/models/advitiseInfoMod/type.go b/models/advitiseInfoMod/type.go new file mode 100644 index 0000000..8186581 --- /dev/null +++ b/models/advitiseInfoMod/type.go @@ -0,0 +1,35 @@ +package advitiseInfoMod + +import ( + "stat/common/mongos" + "time" +) + +var db *mongos.MongoTool + +// AdvitiseInfo 广告素材(V2):素材本身与位置/绑定解耦,可复用到多个 app 的多个位置 +// @table:advitise_info +type AdvitiseInfo struct { + Id int64 `json:"id" bson:"_id"` + Title string `json:"title" bson:"title"` // 广告标题 + FullScreenImgs []string `json:"fullScreenImgs" bson:"fullScreenImgs"` // 全屏封面 + Banner720300Imgs []string `json:"banner720300Imgs" bson:"banner720300Imgs"` // Banner720*300 封面 + Banner720150Imgs []string `json:"banner720150Imgs" bson:"banner720150Imgs"` // Banner720*150 封面 + Banner750700Imgs []string `json:"banner750700Imgs" bson:"banner750700Imgs"` // Banner750*700 封面 + Banner800450Imgs []string `json:"banner800450Imgs" bson:"banner800450Imgs"` // Banner800*450 封面 + PopUps600800Imgs []string `json:"popUps600800Imgs" bson:"popUps600800Imgs"` // 弹窗 600*800 封面 + PopUps512512Imgs []string `json:"popUps512512Imgs" bson:"popUps512512Imgs"` // 弹窗 512*512 封面 + SmallCubeImgs []string `json:"smallCubeImgs" bson:"smallCubeImgs"` // 小方块封面 + VerticalCubeImgs []string `json:"verticalCubeImgs" bson:"verticalCubeImgs"` // 竖方块封面 + RandomNames []string `json:"randomNames" bson:"randomNames"` // 随机名称 + Link string `json:"link" bson:"link"` // 跳转链接 + Status bool `json:"status" bson:"status"` // 是否激活 + StartTime time.Time `json:"startTime" bson:"startTime"` // 开始时间 + EndTime time.Time `json:"endTime" bson:"endTime"` // 结束时间 + Remark string `json:"remark" bson:"remark"` // 备注 + Desc string `json:"desc" bson:"desc"` // 描述 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +type AdvitiseInfoList []*AdvitiseInfo diff --git a/models/advitiseLocationMod/location.go b/models/advitiseLocationMod/location.go new file mode 100644 index 0000000..2ef4100 --- /dev/null +++ b/models/advitiseLocationMod/location.go @@ -0,0 +1,80 @@ +package advitiseLocationMod + +import ( + "context" + "fmt" + "stat/common/log" + "stat/common/mongos" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func Init(ctx context.Context) { + db = mongos.New(mongos.MgoManager, TN.AdvitiseLocation) + indexes := []mongo.IndexModel{ + { + Keys: bson.D{ + {Key: FN.AdvitiseLocation.AppId, Value: 1}, + {Key: FN.AdvitiseLocation.LocId, Value: 1}, + }, + Options: options.Index().SetUnique(true), + }, + {Keys: bson.D{{Key: FN.AdvitiseLocation.CreatedAt, Value: -1}}}, + } + if _, err := db.CreateIndex(ctx, indexes); err != nil { + panic(fmt.Sprintf("(advitiseLocationMod:Init) CreateIndex failed: %+v", err)) + } +} + +func InsertOneAdvitiseLocation(ctx context.Context, doc *AdvitiseLocation) error { + if _, err := db.InsertOne(ctx, doc); err != nil { + log.Warn("advitiseLocationMod InsertOne failed", log.E(err)) + return err + } + return nil +} + +func FindOneAdvitiseLocation(ctx context.Context, filter bson.M) (*AdvitiseLocation, error) { + var doc AdvitiseLocation + if err := db.FindOne(ctx, &doc, filter); err != nil { + return nil, err + } + return &doc, nil +} + +func DelOneAdvitiseLocation(ctx context.Context, filter bson.M) (int64, error) { + count, err := db.DeleteOne(ctx, filter) + if err != nil { + log.Warn("advitiseLocationMod DeleteOne failed", log.E(err), log.Any("filter", filter)) + return 0, err + } + return count, nil +} + +func UpdateOneAdvitiseLocation(ctx context.Context, filter bson.M, update bson.M) error { + if _, err := db.UpdateOne(ctx, filter, update); err != nil { + log.Warn("advitiseLocationMod UpdateOne failed", log.E(err), log.Any("filter", filter)) + return err + } + return nil +} + +func CountAdvitiseLocations(ctx context.Context, filter bson.M) (int64, error) { + count, err := db.Count(ctx, filter) + if err != nil { + log.Warn("advitiseLocationMod Count failed", log.E(err), log.Any("filter", filter)) + return 0, err + } + return count, nil +} + +func FindManyAdvitiseLocations(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (AdvitiseLocationList, error) { + list := make(AdvitiseLocationList, 0) + if err := db.Find(ctx, &list, filter, opts...); err != nil { + log.Warn("advitiseLocationMod Find failed", log.E(err), log.Any("filter", filter)) + return nil, err + } + return list, nil +} diff --git a/models/advitiseLocationMod/tag.go b/models/advitiseLocationMod/tag.go new file mode 100644 index 0000000..f5fb0ff --- /dev/null +++ b/models/advitiseLocationMod/tag.go @@ -0,0 +1,34 @@ +package advitiseLocationMod + +type TagAdvitiseLocation struct { + Id string + AppId string + LocId string + Name string + AdvitiseSize string + ImgSizeDesc string + Remark string + CreatedAt string + UpdatedAt string +} + +var FN struct { + AdvitiseLocation TagAdvitiseLocation +} + +var TN struct { + AdvitiseLocation string +} + +func init() { + FN.AdvitiseLocation.Id = "_id" + FN.AdvitiseLocation.AppId = "appId" + FN.AdvitiseLocation.LocId = "locId" + FN.AdvitiseLocation.Name = "name" + FN.AdvitiseLocation.AdvitiseSize = "advitiseSize" + FN.AdvitiseLocation.ImgSizeDesc = "imgSizeDesc" + FN.AdvitiseLocation.Remark = "remark" + FN.AdvitiseLocation.CreatedAt = "createdAt" + FN.AdvitiseLocation.UpdatedAt = "updatedAt" + TN.AdvitiseLocation = "advitise_location" +} diff --git a/models/advitiseLocationMod/type.go b/models/advitiseLocationMod/type.go new file mode 100644 index 0000000..e4e883f --- /dev/null +++ b/models/advitiseLocationMod/type.go @@ -0,0 +1,38 @@ +package advitiseLocationMod + +import ( + "stat/common/mongos" + "time" +) + +var db *mongos.MongoTool + +type AdvitiseSizeType string + +const ( + AdvitiseSizeFullScreen AdvitiseSizeType = "全屏" // 全屏 + AdvitiseSizeBanner720300 AdvitiseSizeType = "banner720*300" // Banner720*300 + AdvitiseSizeBanner720150 AdvitiseSizeType = "banner720*150" // Banner720*150 + AdvitiseSizeBanner750700 AdvitiseSizeType = "banner750*700" // Banner750*700 + AdvitiseSizeBanner800450 AdvitiseSizeType = "banner800*450" // Banner800*450 + AdvitiseSizePopUps600800 AdvitiseSizeType = "弹窗600*800" // 弹窗 600*800 + AdvitiseSizePopUps512512 AdvitiseSizeType = "弹窗512*512" // 弹窗 512*512 + AdvitiseSizeSmallCube AdvitiseSizeType = "小方块" // 小方块 + AdvitiseSizeVerticalCube AdvitiseSizeType = "竖方块" // 竖方块 +) + +// AdvitiseLocation 广告位(V2):按 (appId, locId) 唯一 +// @table:advitise_location +type AdvitiseLocation struct { + Id int64 `json:"id" bson:"_id"` + AppId int32 `json:"appId" bson:"appId"` // 应用id + LocId int32 `json:"locId" bson:"locId"` // 位置id,外部使用 + Name string `json:"name" bson:"name"` // 位置名称 + AdvitiseSize AdvitiseSizeType `json:"advitiseSize" bson:"advitiseSize"` // 尺寸类型 + ImgSizeDesc string `json:"imgSizeDesc" bson:"imgSizeDesc"` // 图片描述 + Remark string `json:"remark" bson:"remark"` // 备注 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +type AdvitiseLocationList []*AdvitiseLocation diff --git a/models/index.go b/models/index.go index e7b537d..523a42e 100644 --- a/models/index.go +++ b/models/index.go @@ -9,6 +9,11 @@ import ( "stat/models/advertiseLocMod" "stat/models/advertiseMod" "stat/models/advertisestat" + "stat/models/advitiseBindMod" + "stat/models/advitiseClickStatMod" + "stat/models/advitiseDayClickStatMod" + "stat/models/advitiseInfoMod" + "stat/models/advitiseLocationMod" "stat/models/announInfoMod" "stat/models/appInfoMod" "stat/models/appcategory" @@ -109,4 +114,9 @@ func InitIndex() { applicationLocMod.Init(ctx) channelpackage.Init(ctx) cpcDownloadCfg.Init(ctx) + advitiseInfoMod.Init(ctx) + advitiseLocationMod.Init(ctx) + advitiseBindMod.Init(ctx) + advitiseClickStatMod.Init(ctx) + advitiseDayClickStatMod.Init(ctx) } diff --git a/router/advitiseV2.go b/router/advitiseV2.go new file mode 100644 index 0000000..3e60976 --- /dev/null +++ b/router/advitiseV2.go @@ -0,0 +1,40 @@ +package router + +import ( + "stat/api/advitiseV2Api" + + "github.com/gin-gonic/gin" +) + +func advitiseV2Router(router *gin.RouterGroup) { + v2 := router.Group("/advitise/v2") + { + info := v2.Group("/info") + info.POST("/list", advitiseV2Api.InfoList) + info.POST("/add", advitiseV2Api.InfoAdd) + info.POST("/update", advitiseV2Api.InfoUpdate) + info.POST("/del", advitiseV2Api.InfoDel) + + loc := v2.Group("/location") + loc.POST("/list", advitiseV2Api.LocationList) + loc.POST("/add", advitiseV2Api.LocationAdd) + loc.POST("/update", advitiseV2Api.LocationUpdate) + loc.POST("/del", advitiseV2Api.LocationDel) + + bind := v2.Group("/bind") + bind.POST("/list", advitiseV2Api.BindList) + bind.POST("/add", advitiseV2Api.BindAdd) + bind.POST("/update", advitiseV2Api.BindUpdate) + bind.POST("/del", advitiseV2Api.BindDel) + + // 外部无鉴权接口(需与白名单一致) + v2.POST("/get", advitiseV2Api.GetAdvitises) + v2.POST("/click", advitiseV2Api.ClickAdvitise) + + // 统计管理(后台) + v2.POST("/day/click/list", advitiseV2Api.DayClickList) + v2.POST("/day/click/info", advitiseV2Api.DayClickInfo) + v2.POST("/click/list", advitiseV2Api.ClickList) + v2.POST("/day/click/summary", advitiseV2Api.ClickSummary) + } +} diff --git a/router/base.go b/router/base.go index e402434..73761fa 100644 --- a/router/base.go +++ b/router/base.go @@ -103,10 +103,11 @@ func registerRouter(router *gin.Engine) { applicationRouter(rootRouter) // 配置app应用 appcategoryRouter(rootRouter) // app分类 userInviteRouter(rootRouter) // 兼容 + advitiseV2Router(rootRouter) // 广告管理V2(素材/位置/绑定) } // 双端下载落地页(无需登陆鉴权) - router.GET("/rsdownload/:appId/:channel", versionApi.CpcDownload) + router.GET("/download/:appId/:channel", versionApi.CpcDownload) } func setGinSwagger(router *gin.Engine) { diff --git a/service/advertiseSer/advertise.go b/service/advertiseSer/advertise.go index 7977ea7..4fe563e 100644 --- a/service/advertiseSer/advertise.go +++ b/service/advertiseSer/advertise.go @@ -280,19 +280,27 @@ func GetAdvertise(ctx context.Context, param proto.AdvertiseReq) (code stdLog.Co } // 增加广告点击次数 +// 兼容策略:先按 param.Id 精确查;如果广告已过期/删除或不属于该 AppId,回退到 (AppId, LocId) 上的最新一条广告 +// LocId 未传时,回退查询只按 AppId 定位;仍找不到才返回 AdvertiseInvalid func IncAdsClickCount(ctx context.Context, param proto.AdsClickReq) (code stdLog.CodeType, data interface{}, err error) { - // 获取广告信息 - adsInfo, err := advertiseMod.FindAdsById(ctx, *param.Id) - if err != nil || adsInfo.Id == 0 { - code = stdLog.AdvertiseInvalid - return + // 先按 Id 查 + adsInfo, findErr := advertiseMod.FindAdsById(ctx, *param.Id) + needFallback := findErr != nil || adsInfo.Id == 0 || adsInfo.AppId != *param.AppId + if needFallback { + var locId int32 + if param.LocId != nil { + locId = *param.LocId + } + latest, latestErr := advertiseMod.FindLatestAdsByAppAndLoc(ctx, *param.AppId, locId) + if latestErr != nil || latest.Id == 0 { + code = stdLog.AdvertiseInvalid + err = latestErr + return + } + adsInfo = latest } - if adsInfo.AppId != *param.AppId { - code = stdLog.AppInfoError - return - } - // save to redis + // save to redis(用真正命中的广告 Id 统计) now := time.Now().Local() set := redis.GetAdvertiseClickDayHash(now) name := redis.GetAdvertiseClickHourName(adsInfo.AppId, adsInfo.Id, now.Hour()) @@ -301,7 +309,7 @@ func IncAdsClickCount(ctx context.Context, param proto.AdsClickReq) (code stdLog return } // update click+1 to db - if err = advertiseMod.IncAdsClickCount(ctx, *param.Id); err != nil { + if err = advertiseMod.IncAdsClickCount(ctx, adsInfo.Id); err != nil { code = stdLog.Failure return } diff --git a/service/advitiseV2Ser/bind.go b/service/advitiseV2Ser/bind.go new file mode 100644 index 0000000..6a4db45 --- /dev/null +++ b/service/advitiseV2Ser/bind.go @@ -0,0 +1,92 @@ +package advitiseV2Ser + +import ( + "context" + "stat/api/proto" + "stat/common/genId" + "stat/models/advitiseBindMod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func BindList(ctx context.Context, req *proto.AdvitiseBindListReq) (*proto.AdvitiseBindListResp, error) { + resp := &proto.AdvitiseBindListResp{ + Total: 0, + List: make(advitiseBindMod.AdvitiseBindList, 0), + } + filter := bson.M{} + if req.AppId != nil { + filter[advitiseBindMod.FN.AdvitiseBind.AppId] = *req.AppId + } + if req.AdvitiseId != nil { + filter[advitiseBindMod.FN.AdvitiseBind.AdvitiseId] = *req.AdvitiseId + } + if req.LocationId != nil { + filter[advitiseBindMod.FN.AdvitiseBind.LocationId] = *req.LocationId + } + total, err := advitiseBindMod.CountAdvitiseBinds(ctx, filter) + if err != nil { + return nil, err + } + opt := &options.FindOptions{} + opt.SetSort(bson.D{ + {Key: advitiseBindMod.FN.AdvitiseBind.Sort, Value: -1}, + {Key: advitiseBindMod.FN.AdvitiseBind.CreatedAt, Value: -1}, + }) + opt.SetSkip((req.PageNum - 1) * req.PageSize) + opt.SetLimit(req.PageSize) + list, err := advitiseBindMod.FindManyAdvitiseBinds(ctx, filter, opt) + if err != nil { + return nil, err + } + resp.Total = total + resp.List = list + return resp, nil +} + +func BindAdd(ctx context.Context, req *proto.AddAdvitiseBindReq) error { + now := time.Now() + locs := req.LocationId + if locs == nil { + locs = []int32{} + } + doc := &advitiseBindMod.AdvitiseBind{ + Id: genId.GetAdvitiseBindId(), + AppId: req.AppId, + AdvitiseId: req.AdvitiseId, + LocationId: locs, + Sort: req.Sort, + CreatedAt: now, + UpdatedAt: now, + } + return advitiseBindMod.InsertOneAdvitiseBind(ctx, doc) +} + +func BindUpdate(ctx context.Context, req *proto.UpdateAdvitiseBindReq) error { + set := bson.M{ + advitiseBindMod.FN.AdvitiseBind.UpdatedAt: time.Now(), + } + if req.AppId != nil { + set[advitiseBindMod.FN.AdvitiseBind.AppId] = *req.AppId + } + if req.AdvitiseId != nil { + set[advitiseBindMod.FN.AdvitiseBind.AdvitiseId] = *req.AdvitiseId + } + if req.LocationId != nil { + set[advitiseBindMod.FN.AdvitiseBind.LocationId] = *req.LocationId + } + if req.Sort != nil { + set[advitiseBindMod.FN.AdvitiseBind.Sort] = *req.Sort + } + return advitiseBindMod.UpdateOneAdvitiseBind(ctx, + bson.M{advitiseBindMod.FN.AdvitiseBind.Id: req.Id}, + bson.M{"$set": set}, + ) +} + +func BindDel(ctx context.Context, id int64) error { + _, err := advitiseBindMod.DelOneAdvitiseBind(ctx, bson.M{advitiseBindMod.FN.AdvitiseBind.Id: id}) + return err +} diff --git a/service/advitiseV2Ser/get_click.go b/service/advitiseV2Ser/get_click.go new file mode 100644 index 0000000..36b0c1d --- /dev/null +++ b/service/advitiseV2Ser/get_click.go @@ -0,0 +1,214 @@ +package advitiseV2Ser + +import ( + "context" + "errors" + "stat/api/proto" + "stat/common/log" + "stat/models/advitiseBindMod" + "stat/models/advitiseClickStatMod" + "stat/models/advitiseDayClickStatMod" + "stat/models/advitiseInfoMod" + "stat/models/advitiseLocationMod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// idScale 把 (bindId, locId) 编码进单个 int64 id 的乘数 +// 约束:locId < idScale。int64 上限允许 bindId 最大约 9.2e13,远超实际 LID 生成量 +const idScale = int64(100000) + +// GetAdvitises 外部拉广告:响应结构与老 GetAdvertise (proto.AdvertiseRes) 一致 +// - 每条广告绑定一个 (bindId, locId) 组合 +// - CoverImg 依据 location.AdvitiseSize 选 info 里对应尺寸组的第一张 +// - 返回的 id = bindId*idScale + locId,client 点击时原样回传,后端解码定位 (bind, locId) +// - BindId 也单独返回,给愿意用二维接口的 client 用 +func GetAdvitises(ctx context.Context, req *proto.GetAdvitiseV2Req) (*proto.AdvertiseRes, error) { + resp := &proto.AdvertiseRes{AdvertiseList: make([]*proto.AdvertiseInfo, 0)} + + // 1) 拉 bind + filter := bson.M{advitiseBindMod.FN.AdvitiseBind.AppId: req.AppId} + if req.LocationId != nil { + filter[advitiseBindMod.FN.AdvitiseBind.LocationId] = *req.LocationId + } + opt := &options.FindOptions{} + opt.SetSort(bson.D{ + {Key: advitiseBindMod.FN.AdvitiseBind.Sort, Value: -1}, + {Key: advitiseBindMod.FN.AdvitiseBind.CreatedAt, Value: -1}, + }) + binds, err := advitiseBindMod.FindManyAdvitiseBinds(ctx, filter, opt) + if err != nil { + return nil, err + } + if len(binds) == 0 { + return resp, nil + } + + // 2) 批量拉当前生效 info + advitiseIds := uniqInt64(func() []int64 { + out := make([]int64, 0, len(binds)) + for _, b := range binds { + out = append(out, b.AdvitiseId) + } + return out + }()) + now := time.Now() + infos, err := advitiseInfoMod.FindManyAdvitiseInfos(ctx, bson.M{ + advitiseInfoMod.FN.AdvitiseInfo.Id: bson.M{"$in": advitiseIds}, + advitiseInfoMod.FN.AdvitiseInfo.Status: true, + advitiseInfoMod.FN.AdvitiseInfo.StartTime: bson.M{"$lte": now}, + advitiseInfoMod.FN.AdvitiseInfo.EndTime: bson.M{"$gte": now}, + }) + if err != nil { + return nil, err + } + infoMap := make(map[int64]*advitiseInfoMod.AdvitiseInfo, len(infos)) + for _, info := range infos { + infoMap[info.Id] = info + } + + // 3) 批量拉 location(用于 CoverImg 尺寸匹配) + locIdsAll := uniqInt32(func() []int32 { + out := make([]int32, 0) + for _, b := range binds { + out = append(out, b.LocationId...) + } + return out + }()) + locMap := make(map[int32]*advitiseLocationMod.AdvitiseLocation) + if len(locIdsAll) > 0 { + locs, lerr := advitiseLocationMod.FindManyAdvitiseLocations(ctx, bson.M{ + advitiseLocationMod.FN.AdvitiseLocation.AppId: req.AppId, + advitiseLocationMod.FN.AdvitiseLocation.LocId: bson.M{"$in": locIdsAll}, + }) + if lerr == nil { + for _, l := range locs { + locMap[l.LocId] = l + } + } + } + + // 4) 展开 bind × location 为 item + for _, b := range binds { + info, ok := infoMap[b.AdvitiseId] + if !ok { + continue + } + for _, locId := range b.LocationId { + if req.LocationId != nil && locId != *req.LocationId { + continue + } + var size advitiseLocationMod.AdvitiseSizeType + if loc, ok := locMap[locId]; ok && loc != nil { + size = loc.AdvitiseSize + } + resp.AdvertiseList = append(resp.AdvertiseList, &proto.AdvertiseInfo{ + Id: b.Id*idScale + int64(locId), + Title: info.Title, + CoverImg: pickCoverImg(info, size), + LocId: locId, + JumpType: 0, // V2 素材未维护跳转类型,默认外部浏览器 + Link: info.Link, + Sort: b.Sort, + Type: 0, + Tags: "", + Remark: info.Remark, + AmbientColor: "", + BindId: b.Id, + }) + } + } + return resp, nil +} + +// pickCoverImg 按位置尺寸选一张封面;取不到就用任一非空数组的第一张兜底 +func pickCoverImg(info *advitiseInfoMod.AdvitiseInfo, size advitiseLocationMod.AdvitiseSizeType) string { + var arr []string + switch size { + case advitiseLocationMod.AdvitiseSizeFullScreen: + arr = info.FullScreenImgs + case advitiseLocationMod.AdvitiseSizeBanner720300: + arr = info.Banner720300Imgs + case advitiseLocationMod.AdvitiseSizeBanner720150: + arr = info.Banner720150Imgs + case advitiseLocationMod.AdvitiseSizeBanner750700: + arr = info.Banner750700Imgs + case advitiseLocationMod.AdvitiseSizeBanner800450: + arr = info.Banner800450Imgs + case advitiseLocationMod.AdvitiseSizePopUps600800: + arr = info.PopUps600800Imgs + case advitiseLocationMod.AdvitiseSizePopUps512512: + arr = info.PopUps512512Imgs + case advitiseLocationMod.AdvitiseSizeSmallCube: + arr = info.SmallCubeImgs + case advitiseLocationMod.AdvitiseSizeVerticalCube: + arr = info.VerticalCubeImgs + } + if len(arr) > 0 { + return arr[0] + } + // 兜底:任一非空数组的第一张 + for _, cand := range [][]string{ + info.FullScreenImgs, info.Banner720300Imgs, info.Banner720150Imgs, + info.Banner750700Imgs, info.Banner800450Imgs, + info.PopUps600800Imgs, info.PopUps512512Imgs, + info.SmallCubeImgs, info.VerticalCubeImgs, + } { + if len(cand) > 0 { + return cand[0] + } + } + return "" +} + +func uniqInt64(in []int64) []int64 { + seen := make(map[int64]struct{}, len(in)) + out := make([]int64, 0, len(in)) + for _, v := range in { + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + out = append(out, v) + } + return out +} + +func uniqInt32(in []int32) []int32 { + seen := make(map[int32]struct{}, len(in)) + out := make([]int32, 0, len(in)) + for _, v := range in { + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + out = append(out, v) + } + return out +} + +// ClickAdvitise 点击回传:media 逻辑 +// 1. advitise_click_stat upsert +1 (appId, advitiseId, locationId, day) +// 2. advitise_day_click_stat upsert +1 (advitiseId, day) +func ClickAdvitise(ctx context.Context, req *proto.ClickAdvitiseV2Req) error { + bindId := req.Id / idScale + location := int32(req.Id % idScale) + bind, err := advitiseBindMod.FindOneAdvitiseBind(ctx, bson.M{advitiseBindMod.FN.AdvitiseBind.Id: bindId}) + if err != nil || bind == nil { + return errors.New("bind not found") + } + + day := time.Now().Truncate(24 * time.Hour) + + if err := advitiseClickStatMod.InsertOrUpdate(ctx, bind.AppId, bind.AdvitiseId, location, day); err != nil { + log.Warn("v2 click: fine stat upsert failed", log.E(err)) + return err + } + if err := advitiseDayClickStatMod.InsertOrUpdate(ctx, bind.AdvitiseId, day); err != nil { + // 汇总表失败不阻断细维度已成功的记录 + log.Warn("v2 click: day rollup upsert failed", log.E(err)) + } + return nil +} diff --git a/service/advitiseV2Ser/info.go b/service/advitiseV2Ser/info.go new file mode 100644 index 0000000..6ae2216 --- /dev/null +++ b/service/advitiseV2Ser/info.go @@ -0,0 +1,144 @@ +package advitiseV2Ser + +import ( + "context" + "stat/api/proto" + "stat/common/genId" + "stat/models/advitiseInfoMod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func InfoList(ctx context.Context, req *proto.AdvitiseInfoListReq) (*proto.AdvitiseInfoListResp, error) { + resp := &proto.AdvitiseInfoListResp{ + Total: 0, + List: make(advitiseInfoMod.AdvitiseInfoList, 0), + } + filter := bson.M{} + if req.Title != "" { + filter[advitiseInfoMod.FN.AdvitiseInfo.Title] = bson.M{"$regex": req.Title, "$options": "i"} + } + if req.Remark != "" { + filter[advitiseInfoMod.FN.AdvitiseInfo.Remark] = bson.M{"$regex": req.Remark, "$options": "i"} + } + if req.Status != nil { + filter[advitiseInfoMod.FN.AdvitiseInfo.Status] = *req.Status + } + total, err := advitiseInfoMod.CountAdvitiseInfos(ctx, filter) + if err != nil { + return nil, err + } + opt := &options.FindOptions{} + opt.SetSort(bson.D{{Key: advitiseInfoMod.FN.AdvitiseInfo.CreatedAt, Value: -1}}) + opt.SetSkip((req.PageNum - 1) * req.PageSize) + opt.SetLimit(req.PageSize) + list, err := advitiseInfoMod.FindManyAdvitiseInfos(ctx, filter, opt) + if err != nil { + return nil, err + } + resp.Total = total + resp.List = list + return resp, nil +} + +func InfoAdd(ctx context.Context, req *proto.AddAdvitiseInfoReq) error { + now := time.Now() + doc := &advitiseInfoMod.AdvitiseInfo{ + Id: genId.GetAdvitiseInfoId(), + Title: req.Title, + FullScreenImgs: emptyIfNil(req.FullScreenImgs), + Banner720300Imgs: emptyIfNil(req.Banner720300Imgs), + Banner720150Imgs: emptyIfNil(req.Banner720150Imgs), + Banner750700Imgs: emptyIfNil(req.Banner750700Imgs), + Banner800450Imgs: emptyIfNil(req.Banner800450Imgs), + PopUps600800Imgs: emptyIfNil(req.PopUps600800Imgs), + PopUps512512Imgs: emptyIfNil(req.PopUps512512Imgs), + SmallCubeImgs: emptyIfNil(req.SmallCubeImgs), + VerticalCubeImgs: emptyIfNil(req.VerticalCubeImgs), + RandomNames: emptyIfNil(req.RandomNames), + Link: req.Link, + Status: req.Status, + StartTime: req.StartTime, + EndTime: req.EndTime, + Remark: req.Remark, + Desc: req.Desc, + CreatedAt: now, + UpdatedAt: now, + } + return advitiseInfoMod.InsertOneAdvitiseInfo(ctx, doc) +} + +func InfoUpdate(ctx context.Context, req *proto.UpdateAdvitiseInfoReq) error { + set := bson.M{ + advitiseInfoMod.FN.AdvitiseInfo.UpdatedAt: time.Now(), + } + if req.Title != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.Title] = *req.Title + } + if req.FullScreenImgs != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.FullScreenImgs] = *req.FullScreenImgs + } + if req.Banner720300Imgs != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.Banner720300Imgs] = *req.Banner720300Imgs + } + if req.Banner720150Imgs != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.Banner720150Imgs] = *req.Banner720150Imgs + } + if req.Banner750700Imgs != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.Banner750700Imgs] = *req.Banner750700Imgs + } + if req.Banner800450Imgs != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.Banner800450Imgs] = *req.Banner800450Imgs + } + if req.PopUps600800Imgs != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.PopUps600800Imgs] = *req.PopUps600800Imgs + } + if req.PopUps512512Imgs != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.PopUps512512Imgs] = *req.PopUps512512Imgs + } + if req.SmallCubeImgs != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.SmallCubeImgs] = *req.SmallCubeImgs + } + if req.VerticalCubeImgs != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.VerticalCubeImgs] = *req.VerticalCubeImgs + } + if req.RandomNames != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.RandomNames] = *req.RandomNames + } + if req.Link != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.Link] = *req.Link + } + if req.Status != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.Status] = *req.Status + } + if req.StartTime != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.StartTime] = *req.StartTime + } + if req.EndTime != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.EndTime] = *req.EndTime + } + if req.Remark != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.Remark] = *req.Remark + } + if req.Desc != nil { + set[advitiseInfoMod.FN.AdvitiseInfo.Desc] = *req.Desc + } + return advitiseInfoMod.UpdateOneAdvitiseInfo(ctx, + bson.M{advitiseInfoMod.FN.AdvitiseInfo.Id: req.Id}, + bson.M{"$set": set}, + ) +} + +func InfoDel(ctx context.Context, id int64) error { + _, err := advitiseInfoMod.DelOneAdvitiseInfo(ctx, bson.M{advitiseInfoMod.FN.AdvitiseInfo.Id: id}) + return err +} + +func emptyIfNil(s []string) []string { + if s == nil { + return []string{} + } + return s +} diff --git a/service/advitiseV2Ser/location.go b/service/advitiseV2Ser/location.go new file mode 100644 index 0000000..1c0461a --- /dev/null +++ b/service/advitiseV2Ser/location.go @@ -0,0 +1,93 @@ +package advitiseV2Ser + +import ( + "context" + "stat/api/proto" + "stat/common/genId" + "stat/models/advitiseLocationMod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func LocationList(ctx context.Context, req *proto.AdvitiseLocationListReq) (*proto.AdvitiseLocationListResp, error) { + resp := &proto.AdvitiseLocationListResp{ + Total: 0, + List: make(advitiseLocationMod.AdvitiseLocationList, 0), + } + filter := bson.M{} + if req.AppId != nil { + filter[advitiseLocationMod.FN.AdvitiseLocation.AppId] = *req.AppId + } + if req.LocId != nil { + filter[advitiseLocationMod.FN.AdvitiseLocation.LocId] = *req.LocId + } + if req.Name != "" { + filter[advitiseLocationMod.FN.AdvitiseLocation.Name] = bson.M{"$regex": req.Name, "$options": "i"} + } + total, err := advitiseLocationMod.CountAdvitiseLocations(ctx, filter) + if err != nil { + return nil, err + } + opt := &options.FindOptions{} + opt.SetSort(bson.D{{Key: advitiseLocationMod.FN.AdvitiseLocation.CreatedAt, Value: -1}}) + opt.SetSkip((req.PageNum - 1) * req.PageSize) + opt.SetLimit(req.PageSize) + list, err := advitiseLocationMod.FindManyAdvitiseLocations(ctx, filter, opt) + if err != nil { + return nil, err + } + resp.Total = total + resp.List = list + return resp, nil +} + +func LocationAdd(ctx context.Context, req *proto.AddAdvitiseLocationReq) error { + now := time.Now() + doc := &advitiseLocationMod.AdvitiseLocation{ + Id: genId.GetAdvitiseLocationId(), + AppId: req.AppId, + LocId: req.LocId, + Name: req.Name, + AdvitiseSize: req.AdvitiseSize, + ImgSizeDesc: req.ImgSizeDesc, + Remark: req.Remark, + CreatedAt: now, + UpdatedAt: now, + } + return advitiseLocationMod.InsertOneAdvitiseLocation(ctx, doc) +} + +func LocationUpdate(ctx context.Context, req *proto.UpdateAdvitiseLocationReq) error { + set := bson.M{ + advitiseLocationMod.FN.AdvitiseLocation.UpdatedAt: time.Now(), + } + if req.AppId != nil { + set[advitiseLocationMod.FN.AdvitiseLocation.AppId] = *req.AppId + } + if req.LocId != nil { + set[advitiseLocationMod.FN.AdvitiseLocation.LocId] = *req.LocId + } + if req.Name != nil { + set[advitiseLocationMod.FN.AdvitiseLocation.Name] = *req.Name + } + if req.AdvitiseSize != nil { + set[advitiseLocationMod.FN.AdvitiseLocation.AdvitiseSize] = *req.AdvitiseSize + } + if req.ImgSizeDesc != nil { + set[advitiseLocationMod.FN.AdvitiseLocation.ImgSizeDesc] = *req.ImgSizeDesc + } + if req.Remark != nil { + set[advitiseLocationMod.FN.AdvitiseLocation.Remark] = *req.Remark + } + return advitiseLocationMod.UpdateOneAdvitiseLocation(ctx, + bson.M{advitiseLocationMod.FN.AdvitiseLocation.Id: req.Id}, + bson.M{"$set": set}, + ) +} + +func LocationDel(ctx context.Context, id int64) error { + _, err := advitiseLocationMod.DelOneAdvitiseLocation(ctx, bson.M{advitiseLocationMod.FN.AdvitiseLocation.Id: id}) + return err +} diff --git a/service/advitiseV2Ser/stat.go b/service/advitiseV2Ser/stat.go new file mode 100644 index 0000000..287b22d --- /dev/null +++ b/service/advitiseV2Ser/stat.go @@ -0,0 +1,285 @@ +package advitiseV2Ser + +import ( + "context" + "stat/api/proto" + "stat/models/advitiseClickStatMod" + "stat/models/advitiseDayClickStatMod" + "stat/models/advitiseInfoMod" + "stat/models/advitiseLocationMod" + "stat/models/appInfoMod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const statDateLayout = "2006-01-02" + +func parseDateRange(startS, endS string) (start, end time.Time, ok bool) { + // 允许两种格式:yyyy-MM-dd 和 yyyy-MM-dd HH:mm:ss + tryLayouts := []string{"2006-01-02 15:04:05", "2006-01-02"} + if startS != "" { + for _, l := range tryLayouts { + if t, err := time.ParseInLocation(l, startS, time.Local); err == nil { + start = t.Truncate(24 * time.Hour) + ok = true + break + } + } + } + if endS != "" { + for _, l := range tryLayouts { + if t, err := time.ParseInLocation(l, endS, time.Local); err == nil { + end = t.Truncate(24 * time.Hour) + ok = true + break + } + } + } + return +} + +// DayClickList 广告每日点击汇总列表(advitise_day_click_stat) +func DayClickList(ctx context.Context, req *proto.AdvitiseDayClickListReq) (*proto.AdvitiseDayClickListResp, error) { + resp := &proto.AdvitiseDayClickListResp{List: make([]*proto.AdvitiseDayClickItem, 0)} + filter := bson.M{} + if req.Id != nil { + filter[advitiseDayClickStatMod.FN.AdvitiseDayClickStat.AdvitiseId] = *req.Id + } + start, end, _ := parseDateRange(req.StatStartDate, req.StatEndDate) + dateFilter := bson.M{} + if !start.IsZero() { + dateFilter["$gte"] = start + } + if !end.IsZero() { + dateFilter["$lte"] = end + } + if len(dateFilter) > 0 { + filter[advitiseDayClickStatMod.FN.AdvitiseDayClickStat.StatDate] = dateFilter + } + + total, err := advitiseDayClickStatMod.Count(ctx, filter) + if err != nil { + return nil, err + } + opt := &options.FindOptions{} + opt.SetSort(bson.D{{Key: advitiseDayClickStatMod.FN.AdvitiseDayClickStat.StatDate, Value: -1}}) + opt.SetSkip((req.PageNum - 1) * req.PageSize) + opt.SetLimit(req.PageSize) + list, err := advitiseDayClickStatMod.FindMany(ctx, filter, opt) + if err != nil { + return nil, err + } + // 批量拉广告标题 + advIds := make([]int64, 0, len(list)) + for _, r := range list { + advIds = append(advIds, r.AdvitiseId) + } + nameMap := loadInfoNames(ctx, advIds) + + for _, r := range list { + resp.List = append(resp.List, &proto.AdvitiseDayClickItem{ + AdsId: r.AdvitiseId, + AdsName: nameMap[r.AdvitiseId], + Clicks: r.Clicks, + StatDate: r.StatDate.Format(statDateLayout), + }) + } + resp.Total = total + return resp, nil +} + +// ClickList 广告细维度点击列表(advitise_click_stat)。既用于「每日点击详情」也用于「广告位查看统计」入口 +func ClickList(ctx context.Context, req *proto.AdvitiseClickListReq) (*proto.AdvitiseClickListResp, error) { + resp := &proto.AdvitiseClickListResp{List: make([]*proto.AdvitiseClickItem, 0)} + filter := bson.M{} + if req.AdsId != nil { + filter[advitiseClickStatMod.FN.AdvitiseClickStat.AdvitiseId] = *req.AdsId + } + if req.AppId != nil { + filter[advitiseClickStatMod.FN.AdvitiseClickStat.AppId] = *req.AppId + } + if req.LocationId != nil { + filter[advitiseClickStatMod.FN.AdvitiseClickStat.LocationId] = *req.LocationId + } + start, end, _ := parseDateRange(req.StatStartDate, req.StatEndDate) + dateFilter := bson.M{} + if !start.IsZero() { + dateFilter["$gte"] = start + } + if !end.IsZero() { + dateFilter["$lte"] = end + } + if len(dateFilter) > 0 { + filter[advitiseClickStatMod.FN.AdvitiseClickStat.StatDate] = dateFilter + } + + total, err := advitiseClickStatMod.Count(ctx, filter) + if err != nil { + return nil, err + } + opt := &options.FindOptions{} + opt.SetSort(bson.D{{Key: advitiseClickStatMod.FN.AdvitiseClickStat.StatDate, Value: -1}}) + opt.SetSkip((req.PageNum - 1) * req.PageSize) + opt.SetLimit(req.PageSize) + list, err := advitiseClickStatMod.FindMany(ctx, filter, opt) + if err != nil { + return nil, err + } + + // 批量映射名字 + advIds := make([]int64, 0) + appIds := make(map[int32]bool) + locIds := make(map[int32]bool) + for _, r := range list { + advIds = append(advIds, r.AdvitiseId) + appIds[r.AppId] = true + locIds[r.LocationId] = true + } + advNameMap := loadInfoNames(ctx, advIds) + appNameMap, _ := appInfoMod.GetAllAppIdNameMap(ctx) + // 位置名字 — 用 (appId, locId) 组合查 + type locKey struct { + appId int32 + locId int32 + } + locNameMap := make(map[locKey]string) + if len(locIds) > 0 { + locIdsSlice := make([]int32, 0, len(locIds)) + for id := range locIds { + locIdsSlice = append(locIdsSlice, id) + } + appIdsSlice := make([]int32, 0, len(appIds)) + for id := range appIds { + appIdsSlice = append(appIdsSlice, id) + } + locs, lerr := advitiseLocationMod.FindManyAdvitiseLocations(ctx, bson.M{ + advitiseLocationMod.FN.AdvitiseLocation.AppId: bson.M{"$in": appIdsSlice}, + advitiseLocationMod.FN.AdvitiseLocation.LocId: bson.M{"$in": locIdsSlice}, + }) + if lerr == nil { + for _, l := range locs { + locNameMap[locKey{appId: l.AppId, locId: l.LocId}] = l.Name + } + } + } + + for _, r := range list { + resp.List = append(resp.List, &proto.AdvitiseClickItem{ + AppId: r.AppId, + AppName: appNameMap[r.AppId], + AdsId: r.AdvitiseId, + AdsName: advNameMap[r.AdvitiseId], + LocationId: r.LocationId, + LocationName: locNameMap[locKey{appId: r.AppId, locId: r.LocationId}], + Clicks: r.Clicks, + StatDate: r.StatDate.Format(statDateLayout), + }) + } + resp.Total = total + return resp, nil +} + +// ClickSummary 按时间范围汇总广告点击量,分页按 clicks desc +// aggregate pipeline: $match → $group by advitiseId → $sort clicks desc → $facet(list, total, grandTotal) +func ClickSummary(ctx context.Context, req *proto.AdvitiseClickSummaryReq) (*proto.AdvitiseClickSummaryResp, error) { + resp := &proto.AdvitiseClickSummaryResp{List: make([]*proto.AdvitiseClickSummaryItem, 0)} + + match := bson.M{} + if req.Id != nil { + match[advitiseClickStatMod.FN.AdvitiseClickStat.AdvitiseId] = *req.Id + } + if req.AppId != nil { + match[advitiseClickStatMod.FN.AdvitiseClickStat.AppId] = *req.AppId + } + if req.LocationId != nil { + match[advitiseClickStatMod.FN.AdvitiseClickStat.LocationId] = *req.LocationId + } + start, end, _ := parseDateRange(req.StatStartDate, req.StatEndDate) + dateFilter := bson.M{} + if !start.IsZero() { + dateFilter["$gte"] = start + } + if !end.IsZero() { + dateFilter["$lte"] = end + } + if len(dateFilter) > 0 { + match[advitiseClickStatMod.FN.AdvitiseClickStat.StatDate] = dateFilter + } + + pipeline := []bson.M{ + {"$match": match}, + {"$group": bson.M{ + "_id": "$" + advitiseClickStatMod.FN.AdvitiseClickStat.AdvitiseId, + "totalClicks": bson.M{"$sum": "$" + advitiseClickStatMod.FN.AdvitiseClickStat.Clicks}, + }}, + {"$facet": bson.M{ + "list": []bson.M{ + {"$sort": bson.M{"totalClicks": -1}}, + {"$skip": (req.PageNum - 1) * req.PageSize}, + {"$limit": req.PageSize}, + }, + "total": []bson.M{{"$count": "count"}}, + "grandTotal": []bson.M{{"$group": bson.M{"_id": nil, "sum": bson.M{"$sum": "$totalClicks"}}}}, + }}, + } + + type facetRow struct { + List []struct { + Id int64 `bson:"_id"` + TotalClicks int64 `bson:"totalClicks"` + } `bson:"list"` + Total []struct { + Count int64 `bson:"count"` + } `bson:"total"` + GrandTotal []struct { + Sum int64 `bson:"sum"` + } `bson:"grandTotal"` + } + rows := make([]facetRow, 0, 1) + if err := advitiseClickStatMod.Aggregate(ctx, &rows, pipeline); err != nil { + return nil, err + } + var facet facetRow + if len(rows) > 0 { + facet = rows[0] + } + if len(facet.Total) > 0 { + resp.Total = facet.Total[0].Count + } + if len(facet.GrandTotal) > 0 { + resp.GrandTotal = facet.GrandTotal[0].Sum + } + + advIds := make([]int64, 0, len(facet.List)) + for _, r := range facet.List { + advIds = append(advIds, r.Id) + } + nameMap := loadInfoNames(ctx, advIds) + for _, r := range facet.List { + resp.List = append(resp.List, &proto.AdvitiseClickSummaryItem{ + AdsId: r.Id, + AdsName: nameMap[r.Id], + TotalClicks: r.TotalClicks, + }) + } + return resp, nil +} + +func loadInfoNames(ctx context.Context, advIds []int64) map[int64]string { + m := make(map[int64]string) + if len(advIds) == 0 { + return m + } + infos, err := advitiseInfoMod.FindManyAdvitiseInfos(ctx, bson.M{ + advitiseInfoMod.FN.AdvitiseInfo.Id: bson.M{"$in": advIds}, + }) + if err != nil { + return m + } + for _, info := range infos { + m[info.Id] = info.Title + } + return m +} diff --git a/statg/config.go b/statg/config.go index 9be11f7..ac96cd0 100644 --- a/statg/config.go +++ b/statg/config.go @@ -51,8 +51,9 @@ type GlobalConfig struct { ManhuaPackageUrl string `json:"manhuaPackageUrl"` } `json:"url"` ProxyUrl struct { - File string `json:"file"` - FileRes string `json:"fileRes"` + File string `json:"file"` + FileRes string `json:"fileRes"` + FileRes1 string `json:"fileRes1"` } `json:"proxyUrl"` KafkaServer bool `json:"kafkaServer"` Host string `json:"host"`