增加双端下载
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package proto
|
||||
|
||||
import "stat/models/cpcDownloadCfg"
|
||||
|
||||
// CpcDownloadCfgListReq 列表请求
|
||||
type CpcDownloadCfgListReq struct {
|
||||
AppId *int32 `json:"appId" form:"appId"` // 可选,按 appId 过滤
|
||||
Channel string `json:"channel" form:"channel"` // 可选,按 channel 模糊匹配
|
||||
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=200"`
|
||||
}
|
||||
|
||||
// CpcDownloadCfgListResp 列表返回
|
||||
type CpcDownloadCfgListResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List cpcDownloadCfg.CpcDownloadCfgList `json:"list"`
|
||||
}
|
||||
|
||||
// AddCpcDownloadCfgReq 新增请求
|
||||
type AddCpcDownloadCfgReq struct {
|
||||
AppId int32 `json:"appId" binding:"required"`
|
||||
Channel string `json:"channel"`
|
||||
LandingPage string `json:"landingPage"`
|
||||
DownloadUrl string `json:"downloadUrl"`
|
||||
IosDownSuff string `json:"iosDownSuff"`
|
||||
Remark string `json:"remark"`
|
||||
Status bool `json:"status"`
|
||||
}
|
||||
|
||||
// UpdateCpcDownloadCfgReq 更新请求
|
||||
type UpdateCpcDownloadCfgReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
AppId *int32 `json:"appId"`
|
||||
Channel *string `json:"channel"`
|
||||
LandingPage *string `json:"landingPage"`
|
||||
DownloadUrl *string `json:"downloadUrl"`
|
||||
IosDownSuff *string `json:"iosDownSuff"`
|
||||
Remark *string `json:"remark"`
|
||||
Status *bool `json:"status"`
|
||||
}
|
||||
|
||||
// DelCpcDownloadCfgReq 删除请求
|
||||
type DelCpcDownloadCfgReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package versionApi
|
||||
|
||||
import (
|
||||
"stat/api/proto"
|
||||
"stat/common/stdLog"
|
||||
"stat/common/utils"
|
||||
"stat/service/cpcDownloadCfgSer"
|
||||
"stat/service/opLogSer"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags CPC下载配置
|
||||
// @Summary CPC下载配置列表
|
||||
// @Description CPC下载配置列表
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param param body proto.CpcDownloadCfgListReq true "参数列表"
|
||||
// @Success 200 {object} proto.CpcDownloadCfgListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/stat/cpc/download/list [POST]
|
||||
func CpcDownloadCfgList(ctx *gin.Context) {
|
||||
req := &proto.CpcDownloadCfgListReq{}
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := cpcDownloadCfgSer.List(ctx, req)
|
||||
if err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.Failure, err.Error())
|
||||
return
|
||||
}
|
||||
stdLog.ServerJSON(ctx, stdLog.Success, resp)
|
||||
}
|
||||
|
||||
// @Tags CPC下载配置
|
||||
// @Summary 新增CPC下载配置
|
||||
// @Description 新增CPC下载配置
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param param body proto.AddCpcDownloadCfgReq true "参数"
|
||||
// @Success 200 {string} string "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/stat/cpc/download/add [POST]
|
||||
func CpcDownloadCfgAdd(ctx *gin.Context) {
|
||||
account, err := utils.GetAdmin(ctx)
|
||||
if err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error())
|
||||
return
|
||||
}
|
||||
req := &proto.AddCpcDownloadCfgReq{}
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error())
|
||||
return
|
||||
}
|
||||
if err := cpcDownloadCfgSer.Add(ctx, req); err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.Failure, err.Error())
|
||||
return
|
||||
}
|
||||
opLogSer.AddOpLog(ctx, account, true, req)
|
||||
stdLog.ServerJSON(ctx, stdLog.Success, "")
|
||||
}
|
||||
|
||||
// @Tags CPC下载配置
|
||||
// @Summary 更新CPC下载配置
|
||||
// @Description 更新CPC下载配置
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param param body proto.UpdateCpcDownloadCfgReq true "参数"
|
||||
// @Success 200 {string} string "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/stat/cpc/download/update [POST]
|
||||
func CpcDownloadCfgUpdate(ctx *gin.Context) {
|
||||
account, err := utils.GetAdmin(ctx)
|
||||
if err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error())
|
||||
return
|
||||
}
|
||||
req := &proto.UpdateCpcDownloadCfgReq{}
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error())
|
||||
return
|
||||
}
|
||||
if err := cpcDownloadCfgSer.Update(ctx, req); err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.Failure, err.Error())
|
||||
return
|
||||
}
|
||||
opLogSer.AddOpLog(ctx, account, true, req)
|
||||
stdLog.ServerJSON(ctx, stdLog.Success, "")
|
||||
}
|
||||
|
||||
// @Tags CPC下载配置
|
||||
// @Summary 删除CPC下载配置
|
||||
// @Description 删除CPC下载配置
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param param body proto.DelCpcDownloadCfgReq true "参数"
|
||||
// @Success 200 {string} string "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/stat/cpc/download/del [POST]
|
||||
func CpcDownloadCfgDel(ctx *gin.Context) {
|
||||
account, err := utils.GetAdmin(ctx)
|
||||
if err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error())
|
||||
return
|
||||
}
|
||||
req := &proto.DelCpcDownloadCfgReq{}
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error())
|
||||
return
|
||||
}
|
||||
if err := cpcDownloadCfgSer.Del(ctx, req.Id); err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.Failure, err.Error())
|
||||
return
|
||||
}
|
||||
opLogSer.AddOpLog(ctx, account, true, req)
|
||||
stdLog.ServerJSON(ctx, stdLog.Success, "")
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package versionApi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"stat/common/stdLog"
|
||||
"stat/service/cpcDownloadCfgSer"
|
||||
"stat/service/versionSer"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CpcDownload 双端下载
|
||||
// @Tags 双端下载
|
||||
// @Summary 双端下载
|
||||
// @Description 记录下载历史。android:302跳转到下载地址(优先按appId+channel取配置,否则取最新);ios:返回mobileconfig配置文件
|
||||
// @Produce octet-stream
|
||||
// @Param appId path int true "产品AppId"
|
||||
// @Param channel path string true "渠道(json字符串)"
|
||||
// @Param tid query string false "埋点traceID"
|
||||
// @Success 200 {file} file "ios返回mobileconfig文件"
|
||||
// @Success 302 {string} string "android跳转下载地址"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /rsdownload/{appId}/{channel} [get]
|
||||
func CpcDownload(ctx *gin.Context) {
|
||||
// 第一个参数 appId
|
||||
appId, err := strconv.Atoi(ctx.Param("appId"))
|
||||
if err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error())
|
||||
return
|
||||
}
|
||||
// 第二个参数 channel(渠道,json字符串)
|
||||
channel := ctx.Param("channel")
|
||||
|
||||
userAgent := ctx.Request.Header.Get("User-Agent")
|
||||
isIos := strings.Contains(userAgent, "iPhone") || strings.Contains(userAgent, "iPad")
|
||||
|
||||
// 记录下载历史(双端都记)
|
||||
versionSer.RecordCpcDownload(ctx, isIos, int32(appId), channel, ctx.ClientIP(), userAgent)
|
||||
|
||||
// ios: 优先按appId和渠道查询双端配置的落地页,配置了落地页则301跳转,否则返回mobileconfig配置文件
|
||||
if isIos {
|
||||
landingPage := cpcDownloadCfgSer.GetLandingPage(ctx, int32(appId), channel)
|
||||
if len(landingPage) > 0 {
|
||||
// 落地页未带dc参数时,拼上当前渠道
|
||||
if !strings.Contains(landingPage, "dc=") {
|
||||
if strings.Contains(landingPage, "?") {
|
||||
landingPage = landingPage + "&dc=" + channel
|
||||
} else {
|
||||
landingPage = landingPage + "?dc=" + channel
|
||||
}
|
||||
}
|
||||
ctx.Redirect(http.StatusMovedPermanently, landingPage)
|
||||
return
|
||||
}
|
||||
cfg := versionSer.IosMobileconfigByAppId(int32(appId))
|
||||
if cfg == nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.Failure, "ios config不存在")
|
||||
return
|
||||
}
|
||||
pc, dc, _ := versionSer.ParseChannel(channel)
|
||||
writeMobileconfig(ctx, cfg, dc, pc)
|
||||
return
|
||||
}
|
||||
|
||||
// android: 优先按appId和channel查询配置的下载链接,没有则使用默认最新下载地址
|
||||
downloadUrl := cpcDownloadCfgSer.GetDownloadUrl(ctx, int32(appId), channel)
|
||||
if downloadUrl == "" {
|
||||
downloadUrl = versionSer.LatestAndroidDownloadUrl(int32(appId))
|
||||
}
|
||||
if downloadUrl == "" {
|
||||
stdLog.ServerJSON(ctx, stdLog.Failure, "android下载地址不存在")
|
||||
return
|
||||
}
|
||||
ctx.Redirect(http.StatusFound, downloadUrl)
|
||||
}
|
||||
|
||||
// writeMobileconfig 将 mobileconfig 文件以附件形式写入响应
|
||||
func writeMobileconfig(ctx *gin.Context, cfg *versionSer.IosMobileconfigCfg, dc, pc string) {
|
||||
data, fileName := versionSer.BuildMobileconfig(cfg, dc, pc)
|
||||
if fileName == "" {
|
||||
fileName = "madou.mobileconfig"
|
||||
}
|
||||
ctx.Header("Content-Type", "application/octet-stream")
|
||||
ctx.Header("Content-Disposition", "attachment; filename="+fileName)
|
||||
ctx.Header("Content-Transfer-Encoding", "binary")
|
||||
_, _ = ctx.Writer.Write(data)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ var audioBook *lid.Lid // 有声小说自增器
|
||||
var landpageConfig *lid.Lid // 落地页配置id
|
||||
var appCategory *lid.Lid //
|
||||
var appLocLid *lid.Lid // 应用中心位置id自增器
|
||||
var cpcDownloadCfgLid *lid.Lid // CPC下载配置id自增器
|
||||
|
||||
func Start() {
|
||||
var err error
|
||||
@@ -98,6 +99,11 @@ func Start() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cpcDownloadCfgLid, err = lid.NewLid(collection, "cpc_download_cfg_id", 100)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetNextRoleId() int64 {
|
||||
@@ -159,3 +165,7 @@ func GetAppCategoryId() int64 {
|
||||
func GetNextAppLocId() int64 {
|
||||
return appLocLid.Get()
|
||||
}
|
||||
|
||||
func GetCpcDownloadCfgId() int64 {
|
||||
return cpcDownloadCfgLid.Get()
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ var (
|
||||
"/api/stat/daily-stat/hour",
|
||||
"/api/stat/daily-stat/rank",
|
||||
"/api/stat/daily-stat/total",
|
||||
"/rsdownload/",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package cpcDownloadCfg
|
||||
|
||||
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.CpcDownloadCfg)
|
||||
indexes := []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: FN.CpcDownloadCfg.AppId, Value: 1},
|
||||
{Key: FN.CpcDownloadCfg.Channel, Value: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: FN.CpcDownloadCfg.Status, Value: 1}},
|
||||
},
|
||||
}
|
||||
if _, err := db.CreateIndex(ctx, indexes); err != nil {
|
||||
panic(fmt.Sprintf("(cpcDownloadCfg:Init) CreateIndex failed: %+v", err))
|
||||
}
|
||||
}
|
||||
|
||||
func InsertOneCpcDownloadCfg(ctx context.Context, doc *CpcDownloadCfg) error {
|
||||
if _, err := db.InsertOne(ctx, doc); err != nil {
|
||||
log.Warn("cpcDownloadCfg InsertOne failed", log.E(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindOneCpcDownloadCfg(ctx context.Context, filter bson.M) (*CpcDownloadCfg, error) {
|
||||
var doc CpcDownloadCfg
|
||||
if err := db.FindOne(ctx, &doc, filter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
func DelOneCpcDownloadCfg(ctx context.Context, filter bson.M) (int64, error) {
|
||||
count, err := db.DeleteOne(ctx, filter)
|
||||
if err != nil {
|
||||
log.Warn("cpcDownloadCfg DeleteOne failed", log.E(err), log.Any("filter", filter))
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func UpdateOneCpcDownloadCfg(ctx context.Context, filter bson.M, update bson.M) error {
|
||||
if _, err := db.UpdateOne(ctx, filter, update); err != nil {
|
||||
log.Warn("cpcDownloadCfg UpdateOne failed", log.E(err), log.Any("filter", filter))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CountCpcDownloadCfgs(ctx context.Context, filter bson.M) (int64, error) {
|
||||
count, err := db.Count(ctx, filter)
|
||||
if err != nil {
|
||||
log.Warn("cpcDownloadCfg Count failed", log.E(err), log.Any("filter", filter))
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func FindManyCpcDownloadCfg(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (CpcDownloadCfgList, error) {
|
||||
list := make(CpcDownloadCfgList, 0)
|
||||
if err := db.Find(ctx, &list, filter, opts...); err != nil {
|
||||
log.Warn("cpcDownloadCfg Find failed", log.E(err), log.Any("filter", filter))
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cpcDownloadCfg
|
||||
|
||||
type TagCpcDownloadCfg struct {
|
||||
Id string
|
||||
AppId string
|
||||
Channel string
|
||||
LandingPage string
|
||||
DownloadUrl string
|
||||
IosDownSuff string
|
||||
Remark string
|
||||
Status string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
var FN struct {
|
||||
CpcDownloadCfg TagCpcDownloadCfg
|
||||
}
|
||||
|
||||
var TN struct {
|
||||
CpcDownloadCfg string
|
||||
}
|
||||
|
||||
func init() {
|
||||
FN.CpcDownloadCfg.Id = "_id"
|
||||
FN.CpcDownloadCfg.AppId = "appId"
|
||||
FN.CpcDownloadCfg.Channel = "channel"
|
||||
FN.CpcDownloadCfg.LandingPage = "landingPage"
|
||||
FN.CpcDownloadCfg.DownloadUrl = "downloadUrl"
|
||||
FN.CpcDownloadCfg.IosDownSuff = "iosDownSuff"
|
||||
FN.CpcDownloadCfg.Remark = "remark"
|
||||
FN.CpcDownloadCfg.Status = "status"
|
||||
FN.CpcDownloadCfg.CreatedAt = "createdAt"
|
||||
FN.CpcDownloadCfg.UpdatedAt = "updatedAt"
|
||||
TN.CpcDownloadCfg = "cpc_download_cfg"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cpcDownloadCfg
|
||||
|
||||
import (
|
||||
"stat/common/mongos"
|
||||
"time"
|
||||
)
|
||||
|
||||
var db *mongos.MongoTool
|
||||
|
||||
// CpcDownloadCfg CPC 下载配置: 双端下载落地页 + Android 下载地址 + iOS mobileconfig 文件名
|
||||
// @table:cpcDownloadCfg
|
||||
type CpcDownloadCfg struct {
|
||||
Id int64 `json:"id" bson:"_id"`
|
||||
AppId int32 `json:"appId" bson:"appId"` // appId
|
||||
Channel string `json:"channel" bson:"channel"` // 渠道(json字符串)
|
||||
LandingPage string `json:"landingPage" bson:"landingPage"` // iOS 落地页 URL,优先于 mobileconfig
|
||||
DownloadUrl string `json:"downloadUrl" bson:"downloadUrl"` // Android 下载地址
|
||||
IosDownSuff string `json:"iosDownSuff" bson:"iosDownSuff"` // iOS mobileconfig 文件名(下载后缀)
|
||||
Remark string `json:"remark" bson:"remark"` // 备注
|
||||
Status bool `json:"status" bson:"status"` // 是否启用
|
||||
CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
|
||||
}
|
||||
|
||||
type CpcDownloadCfgList []*CpcDownloadCfg
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"stat/models/channelmod"
|
||||
"stat/models/channelpackage"
|
||||
"stat/models/channelsettlementrecord"
|
||||
"stat/models/cpcDownloadCfg"
|
||||
"stat/models/dailyhourstatmod"
|
||||
"stat/models/dailystatadvertise"
|
||||
"stat/models/dailystatmod"
|
||||
@@ -107,4 +108,5 @@ func InitIndex() {
|
||||
applicationstat.Init(ctx)
|
||||
applicationLocMod.Init(ctx)
|
||||
channelpackage.Init(ctx)
|
||||
cpcDownloadCfg.Init(ctx)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"stat/api/versionApi"
|
||||
"stat/common/enum/consts"
|
||||
"stat/middleware/auth"
|
||||
"stat/middleware/cors"
|
||||
@@ -103,6 +104,9 @@ func registerRouter(router *gin.Engine) {
|
||||
appcategoryRouter(rootRouter) // app分类
|
||||
userInviteRouter(rootRouter) // 兼容
|
||||
}
|
||||
|
||||
// 双端下载落地页(无需登陆鉴权)
|
||||
router.GET("/rsdownload/:appId/:channel", versionApi.CpcDownload)
|
||||
}
|
||||
|
||||
func setGinSwagger(router *gin.Engine) {
|
||||
|
||||
@@ -43,4 +43,13 @@ func versionRouter(router *gin.RouterGroup) {
|
||||
picture.POST("/del", versionApi.DelOneLandpage)
|
||||
picture.POST("/update", versionApi.UpdateOneLandpage)
|
||||
}
|
||||
|
||||
// CPC 下载配置管理
|
||||
cpc := router.Group("/cpc/download")
|
||||
{
|
||||
cpc.POST("/list", versionApi.CpcDownloadCfgList)
|
||||
cpc.POST("/add", versionApi.CpcDownloadCfgAdd)
|
||||
cpc.POST("/update", versionApi.CpcDownloadCfgUpdate)
|
||||
cpc.POST("/del", versionApi.CpcDownloadCfgDel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package cpcDownloadCfgSer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"stat/api/proto"
|
||||
"stat/common/genId"
|
||||
"stat/common/log"
|
||||
"stat/models/cpcDownloadCfg"
|
||||
"stat/service/cacheSer"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
cacheTTL = 2 * time.Minute
|
||||
)
|
||||
|
||||
func cacheKey(appId int32, channel string) string {
|
||||
return fmt.Sprintf("cpc_download_cfg#%d#%s", appId, channel)
|
||||
}
|
||||
|
||||
// findEnabled 按 (appId, channel) 命中启用的配置;命中缓存(包含未找到的负缓存)
|
||||
func findEnabled(ctx context.Context, appId int32, channel string) *cpcDownloadCfg.CpcDownloadCfg {
|
||||
key := cacheKey(appId, channel)
|
||||
if v, ok := cacheSer.Cache.Get(key); ok {
|
||||
if doc, ok := v.(*cpcDownloadCfg.CpcDownloadCfg); ok {
|
||||
return doc
|
||||
}
|
||||
// 负缓存
|
||||
return nil
|
||||
}
|
||||
filter := bson.M{
|
||||
cpcDownloadCfg.FN.CpcDownloadCfg.AppId: appId,
|
||||
cpcDownloadCfg.FN.CpcDownloadCfg.Channel: channel,
|
||||
cpcDownloadCfg.FN.CpcDownloadCfg.Status: true,
|
||||
}
|
||||
doc, err := cpcDownloadCfg.FindOneCpcDownloadCfg(ctx, filter)
|
||||
if err != nil {
|
||||
// 写入负缓存,避免高频未命中击穿
|
||||
cacheSer.Cache.Set(key, false, cacheTTL)
|
||||
return nil
|
||||
}
|
||||
cacheSer.Cache.Set(key, doc, cacheTTL)
|
||||
return doc
|
||||
}
|
||||
|
||||
// GetLandingPage 按 appId+channel 获取 iOS 落地页 URL,无配置返回空字符串
|
||||
func GetLandingPage(ctx context.Context, appId int32, channel string) string {
|
||||
if doc := findEnabled(ctx, appId, channel); doc != nil {
|
||||
return doc.LandingPage
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetDownloadUrl 按 appId+channel 获取 Android 下载地址,无配置返回空字符串
|
||||
func GetDownloadUrl(ctx context.Context, appId int32, channel string) string {
|
||||
if doc := findEnabled(ctx, appId, channel); doc != nil {
|
||||
return doc.DownloadUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetIosDownSuff 按 appId+channel 获取 mobileconfig 文件名
|
||||
func GetIosDownSuff(ctx context.Context, appId int32, channel string) string {
|
||||
if doc := findEnabled(ctx, appId, channel); doc != nil {
|
||||
return doc.IosDownSuff
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func invalidateCache(appId int32, channel string) {
|
||||
cacheSer.Cache.Delete(cacheKey(appId, channel))
|
||||
}
|
||||
|
||||
// List 后台列表
|
||||
func List(ctx context.Context, req *proto.CpcDownloadCfgListReq) (*proto.CpcDownloadCfgListResp, error) {
|
||||
resp := &proto.CpcDownloadCfgListResp{
|
||||
Total: 0,
|
||||
List: make(cpcDownloadCfg.CpcDownloadCfgList, 0),
|
||||
}
|
||||
filter := bson.M{}
|
||||
if req.AppId != nil {
|
||||
filter[cpcDownloadCfg.FN.CpcDownloadCfg.AppId] = *req.AppId
|
||||
}
|
||||
if req.Channel != "" {
|
||||
filter[cpcDownloadCfg.FN.CpcDownloadCfg.Channel] = req.Channel
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[cpcDownloadCfg.FN.CpcDownloadCfg.Status] = *req.Status
|
||||
}
|
||||
|
||||
total, err := cpcDownloadCfg.CountCpcDownloadCfgs(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opt := &options.FindOptions{}
|
||||
opt.SetSort(bson.D{{Key: cpcDownloadCfg.FN.CpcDownloadCfg.CreatedAt, Value: -1}})
|
||||
opt.SetSkip((req.PageNum - 1) * req.PageSize)
|
||||
opt.SetLimit(req.PageSize)
|
||||
list, err := cpcDownloadCfg.FindManyCpcDownloadCfg(ctx, filter, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Total = total
|
||||
resp.List = list
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Add 新增
|
||||
func Add(ctx context.Context, req *proto.AddCpcDownloadCfgReq) error {
|
||||
now := time.Now()
|
||||
doc := &cpcDownloadCfg.CpcDownloadCfg{
|
||||
Id: genId.GetCpcDownloadCfgId(),
|
||||
AppId: req.AppId,
|
||||
Channel: req.Channel,
|
||||
LandingPage: req.LandingPage,
|
||||
DownloadUrl: req.DownloadUrl,
|
||||
IosDownSuff: req.IosDownSuff,
|
||||
Remark: req.Remark,
|
||||
Status: req.Status,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := cpcDownloadCfg.InsertOneCpcDownloadCfg(ctx, doc); err != nil {
|
||||
return err
|
||||
}
|
||||
invalidateCache(doc.AppId, doc.Channel)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update 更新
|
||||
func Update(ctx context.Context, req *proto.UpdateCpcDownloadCfgReq) error {
|
||||
old, err := cpcDownloadCfg.FindOneCpcDownloadCfg(ctx, bson.M{
|
||||
cpcDownloadCfg.FN.CpcDownloadCfg.Id: req.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
set := bson.M{
|
||||
cpcDownloadCfg.FN.CpcDownloadCfg.UpdatedAt: time.Now(),
|
||||
}
|
||||
if req.AppId != nil {
|
||||
set[cpcDownloadCfg.FN.CpcDownloadCfg.AppId] = *req.AppId
|
||||
}
|
||||
if req.Channel != nil {
|
||||
set[cpcDownloadCfg.FN.CpcDownloadCfg.Channel] = *req.Channel
|
||||
}
|
||||
if req.LandingPage != nil {
|
||||
set[cpcDownloadCfg.FN.CpcDownloadCfg.LandingPage] = *req.LandingPage
|
||||
}
|
||||
if req.DownloadUrl != nil {
|
||||
set[cpcDownloadCfg.FN.CpcDownloadCfg.DownloadUrl] = *req.DownloadUrl
|
||||
}
|
||||
if req.IosDownSuff != nil {
|
||||
set[cpcDownloadCfg.FN.CpcDownloadCfg.IosDownSuff] = *req.IosDownSuff
|
||||
}
|
||||
if req.Remark != nil {
|
||||
set[cpcDownloadCfg.FN.CpcDownloadCfg.Remark] = *req.Remark
|
||||
}
|
||||
if req.Status != nil {
|
||||
set[cpcDownloadCfg.FN.CpcDownloadCfg.Status] = *req.Status
|
||||
}
|
||||
if err := cpcDownloadCfg.UpdateOneCpcDownloadCfg(ctx, bson.M{
|
||||
cpcDownloadCfg.FN.CpcDownloadCfg.Id: req.Id,
|
||||
}, bson.M{"$set": set}); err != nil {
|
||||
return err
|
||||
}
|
||||
// 同时清空旧 key 与新 key
|
||||
invalidateCache(old.AppId, old.Channel)
|
||||
if req.AppId != nil || req.Channel != nil {
|
||||
newAppId := old.AppId
|
||||
newChannel := old.Channel
|
||||
if req.AppId != nil {
|
||||
newAppId = *req.AppId
|
||||
}
|
||||
if req.Channel != nil {
|
||||
newChannel = *req.Channel
|
||||
}
|
||||
invalidateCache(newAppId, newChannel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Del 删除
|
||||
func Del(ctx context.Context, id int64) error {
|
||||
old, err := cpcDownloadCfg.FindOneCpcDownloadCfg(ctx, bson.M{
|
||||
cpcDownloadCfg.FN.CpcDownloadCfg.Id: id,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("cpcDownloadCfg Del FindOne failed", log.E(err))
|
||||
}
|
||||
if _, err := cpcDownloadCfg.DelOneCpcDownloadCfg(ctx, bson.M{
|
||||
cpcDownloadCfg.FN.CpcDownloadCfg.Id: id,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if old != nil {
|
||||
invalidateCache(old.AppId, old.Channel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package versionSer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"stat/common/log"
|
||||
"stat/common/utils"
|
||||
"stat/models/downloadCpc"
|
||||
"stat/models/versionMod"
|
||||
"stat/service/cacheSer"
|
||||
"stat/statg"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IosMobileconfigCfg iOS mobileconfig 生成所需的应用级配置
|
||||
type IosMobileconfigCfg struct {
|
||||
AppId int32
|
||||
IosDownSuff string // 下载文件名(如 madou.mobileconfig)
|
||||
H5Origin string
|
||||
H5Target string
|
||||
H5Config string
|
||||
}
|
||||
|
||||
// ParseChannel 解析 channel(json 字符串)为 pc/dc/ch;非法 JSON 时返回空字符串,dc 走 RepairDC
|
||||
func ParseChannel(channel string) (pc, dc, ch string) {
|
||||
if channel == "" {
|
||||
return
|
||||
}
|
||||
var p ChannelParams
|
||||
_ = json.Unmarshal([]byte(channel), &p)
|
||||
dc = utils.RepairDC(p.DC)
|
||||
pc = p.PC
|
||||
ch = p.CH
|
||||
return
|
||||
}
|
||||
|
||||
// RecordCpcDownload 记录 cpc 下载到 landingpage_cpc 表(走异步 writer);相同 IP 1 小时内只记一次
|
||||
func RecordCpcDownload(ctx context.Context, isIos bool, appId int32, channel, ip, userAgent string) {
|
||||
if ip == "" {
|
||||
return
|
||||
}
|
||||
pc, dc, ch := ParseChannel(channel)
|
||||
sysType := "android"
|
||||
if isIos {
|
||||
sysType = "ios"
|
||||
}
|
||||
devType := ""
|
||||
if strings.Contains(userAgent, "iPhone") {
|
||||
devType = "iphone"
|
||||
} else if strings.Contains(userAgent, "iPad") {
|
||||
devType = "ipad"
|
||||
} else if strings.Contains(userAgent, "Android") {
|
||||
devType = "android"
|
||||
}
|
||||
info := &downloadCpc.DownloadCpc{
|
||||
AppId: appId,
|
||||
PC: pc,
|
||||
DC: dc,
|
||||
CH: ch,
|
||||
IP: ip,
|
||||
DevType: devType,
|
||||
SysType: sysType,
|
||||
PeerIP: ip,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if landPageVisit.TryLock(ip, time.Now().Add(landPageVisitIpTimeout)) {
|
||||
cacheSer.Writers.LandPageVisitWriter.Add(info)
|
||||
}
|
||||
}
|
||||
|
||||
// LatestAndroidDownloadUrl 取应用最新 Android 版本的下载链接(第一个 Link),没有返回空字符串
|
||||
func LatestAndroidDownloadUrl(appId int32) string {
|
||||
ctx := context.Background()
|
||||
data, err := versionMod.ButtonClick(ctx, appId, "android", 0)
|
||||
if err != nil || data == nil {
|
||||
log.Warn("LatestAndroidDownloadUrl ButtonClick failed", log.E(err), log.Any("appId", appId))
|
||||
return ""
|
||||
}
|
||||
if len(data.Link) == 0 {
|
||||
return ""
|
||||
}
|
||||
return data.Link[0]
|
||||
}
|
||||
|
||||
// IosMobileconfigByAppId 取应用的 iOS mobileconfig 配置;endpoint 未配置 H5 三件套则返回 nil
|
||||
func IosMobileconfigByAppId(appId int32) *IosMobileconfigCfg {
|
||||
endpoint := statg.GlobalAppIdEndpoints[appId]
|
||||
if endpoint == nil {
|
||||
return nil
|
||||
}
|
||||
if endpoint.H5Origin == "" || endpoint.H5Target == "" || endpoint.H5Config == "" {
|
||||
return nil
|
||||
}
|
||||
return &IosMobileconfigCfg{
|
||||
AppId: appId,
|
||||
IosDownSuff: endpoint.H5Config,
|
||||
H5Origin: endpoint.H5Origin,
|
||||
H5Target: endpoint.H5Target,
|
||||
H5Config: endpoint.H5Config,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildMobileconfig 用 dc/pc 生成 mobileconfig 字节流和文件名
|
||||
func BuildMobileconfig(cfg *IosMobileconfigCfg, dc, pc string) ([]byte, string) {
|
||||
if cfg == nil {
|
||||
return nil, ""
|
||||
}
|
||||
return GenerateConfigFile(cfg.H5Origin, cfg.H5Target, cfg.H5Config, dc, pc)
|
||||
}
|
||||
Reference in New Issue
Block a user