增加双端下载

This commit is contained in:
2026-06-30 16:16:55 +08:00
parent 7444fc5c48
commit bc605f303d
13 changed files with 739 additions and 0 deletions
+46
View File
@@ -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"`
}
+122
View File
@@ -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, "")
}
+89
View File
@@ -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)
}