增加双端下载

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
+204
View File
@@ -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
}
+110
View File
@@ -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)
}