Files
2026-05-26 15:57:55 +08:00

382 lines
11 KiB
Go

package versionSer
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"stat/api/proto"
"stat/common/stdLog"
"stat/common/utils"
"stat/models/channelpackage"
"stat/models/downloadCpc"
downloadinfo "stat/models/downloadinfo"
"stat/models/versionMod"
"stat/service/cacheSer"
"stat/service/crossSer"
"stat/statg"
"strings"
"sync"
"time"
"go.mongodb.org/mongo-driver/bson"
)
type ChannelParams struct {
PC string `json:"i" bson:"i"` //推广码
DC string `json:"dc" bson:"dc"` //商区序列
CH string `json:"ch" bson:"ch"` //渠道
}
var (
appSvrUrlDict = make(map[int32]string)
appSvrIdxDict = make(map[int32]statg.ServerIndex)
once sync.Once
apkGenLock = utils.NewExpiredLock()
landPageVisit = utils.NewExpiredLock()
landPageClick = utils.NewExpiredLock()
landPageVisitIpTimeout = time.Hour // 每个IP,1个小时内无论访问多少次,都算一次
landPageClickIpTimeout = time.Hour // 每个IP,1个小时内无论访问多少次,都算一次
)
func Init() {
once.Do(func() {
for _, endpoint := range statg.GlobalEndpoints {
appSvrIdxDict[endpoint.AppId] = endpoint.OwnerIndex
if url, ok := statg.Conf.InnerServers[endpoint.OwnerIndex]; ok {
appSvrUrlDict[endpoint.AppId] = url
}
}
})
}
// CrossButtonClick 跨服务器点击下载返回最新的下载链接
func CrossButtonClick(ctx context.Context, userAgent string, peerIP string, req *proto.PageButtonClickReq) (resp *proto.PageButtonClickResp, err error) {
req.Ip = peerIP
srvIdx := appSvrIdxDict[req.AppId]
if srvIdx == statg.ServerIndexNone {
return nil, fmt.Errorf("appid %d not found", req.AppId)
}
if srvIdx == statg.Conf.ServerIndex {
return PageButtonClick(ctx, peerIP, userAgent, req)
}
addr := appSvrUrlDict[req.AppId]
if addr == "" {
return nil, fmt.Errorf("appid %d server not found", req.AppId)
}
res := crossSer.CrossAppDownloadRes{}
err = crossSer.InnerRequest(addr+"/api/stat/inner/appDownload", req, &res, false)
if err != nil {
return nil, err
}
if res.Data == nil {
return nil, fmt.Errorf("data not found")
}
return res.Data, nil
}
// PageButtonClick 落地页点击下载返回最新的下载链接
func PageButtonClick(ctx context.Context, userAgent string, peerIP string, req *proto.PageButtonClickReq) (resp *proto.PageButtonClickResp, err error) {
resp = &proto.PageButtonClickResp{}
var channelCode ChannelParams
// TODO 存储记录
_ = json.Unmarshal([]byte(req.Channel), &channelCode)
if req.Ip == "" {
req.Ip = peerIP
}
channelCode.DC = utils.RepairDC(channelCode.DC)
if req.Terminal == "ios" && req.PackageType == versionMod.Light {
var h5Req = &proto.H5ConfigReq{
AppId: req.AppId,
DC: channelCode.DC,
PC: channelCode.PC,
Domain: channelCode.PC,
}
resp.Terminal = req.Terminal
resp.PackageType = req.PackageType
resp.Status = true
resp.LightData, _ = GetH5Config(ctx, h5Req, userAgent, req.Ip)
return
}
downloadInfo := &downloadinfo.DownloadInfoModel{
AppId: req.AppId,
IP: req.Ip,
Domain: req.Domain,
PC: channelCode.PC,
DC: channelCode.DC,
CH: channelCode.CH,
SysType: strings.ToLower(strings.TrimSpace(req.Terminal)),
PackageType: req.PackageType,
PeerIP: peerIP,
UserAgent: userAgent,
CreatedAt: time.Now(),
}
//和APP统一
if req.PackageType == versionMod.Shop {
downloadInfo.SysType = "h5_ios"
}
if landPageClick.TryLock(req.Ip, time.Now().Add(landPageClickIpTimeout)) {
cacheSer.Writers.LandPageClickWriter.Add(downloadInfo)
}
if downloadInfo.IP == "" {
downloadInfo.IP = peerIP
}
var data *versionMod.VersionModel
cacheKey := fmt.Sprintf("version_button#appId:%d#%s#%d", req.AppId, req.Terminal, req.PackageType)
if cache, exists := cacheSer.Cache.Get(cacheKey); exists {
if item, ok := cache.(*versionMod.VersionModel); ok {
data = item
}
}
if data == nil {
data, err = versionMod.ButtonClick(ctx, req.AppId, req.Terminal, req.PackageType)
if err != nil {
return nil, err
}
cacheSer.Cache.Set(cacheKey, data, 2*time.Minute)
}
resp.PackageType = data.PackageType
resp.Status = data.Status
resp.Terminal = data.Terminal
resp.VersionNum = data.VersionNum
resp.BackUrl = data.BackLink
resp.Link = append(resp.Link, data.Link...)
// IOS的TF包管理
if req.PackageType == versionMod.Tf {
// 1. FT包管理
for _, tfInfo := range data.TfInfo {
one := proto.TfInfo{
AppName: tfInfo.AppName,
AppIcon: tfInfo.AppIcon,
URL: tfInfo.URL,
}
resp.Tf = append(resp.Tf, one)
}
if len(resp.Tf) == 0 {
resp.Tf = make([]proto.TfInfo, 0)
for _, v := range data.Link {
tmp := proto.TfInfo{
URL: v,
}
if len(data.BackLink) > 0 {
tmp.BackUrl = data.BackLink
}
resp.Tf = append(resp.Tf, tmp)
}
}
return resp, nil
}
if data.Terminal == "ios" {
// 配置多个随机返回一个
if len(data.Link) >= 1 {
resp.Link = make([]string, 0)
index := rand.Intn(len(data.Link))
resp.Link = append(resp.Link, data.Link[index])
}
return
}
// 新增批量打包逻辑
if req.Terminal == "android" && len(data.Md5) > 0 {
randTime := int64(60*60) + int64(rand.Intn(60*60))
requestPackageTime := time.Now().Add(-time.Second * time.Duration(randTime))
// 下载链接
projectString := ""
beginPath := ""
pathList := strings.Split(strings.Replace(data.Md5, ".apk", "", -1), "_")
if len(pathList) == 3 {
beginPath = strings.TrimSpace(pathList[len(pathList)-1])
} else {
return
}
endpoint := statg.GlobalAppIdEndpoints[req.AppId]
if endpoint != nil {
projectString = fmt.Sprintf("%s/out/%s/%s%s_", endpoint.NameEn, beginPath, endpoint.NameEn, requestPackageTime.Format("20060102")[2:])
}
if projectString != "" {
packageUrlString := statg.Conf.URL.PackageUrl
if req.AppId == 999 && len(statg.Conf.URL.BiliPackageUrl) > 0 {
packageUrlString = statg.Conf.URL.BiliPackageUrl
} else if req.AppId == 777 && len(statg.Conf.URL.HuanqiuPackageUrl) > 0 {
packageUrlString = statg.Conf.URL.HuanqiuPackageUrl
} else if req.AppId == 444 && len(statg.Conf.URL.ManhuaPackageUrl) > 0 {
packageUrlString = statg.Conf.URL.ManhuaPackageUrl
}
packageUrlArray := strings.Split(packageUrlString, ";")
if len(packageUrlArray) > 0 {
if len(channelCode.DC) > 0 { //&&
pack, serr := channelpackage.FindOneChannelPackage(ctx, bson.M{
channelpackage.FN.ChannelPackage.AppId: req.AppId,
channelpackage.FN.ChannelPackage.Channel: channelCode.DC,
channelpackage.FN.ChannelPackage.Version: data.VersionNum,
channelpackage.FN.ChannelPackage.FileName: data.Md5,
})
if serr == nil || serr.Code == stdLog.DataNotExist {
if pack != nil && pack.Status == channelpackage.ChanApkStatusSuccess {
pass, links := ApkCheckLinks(pack, packageUrlArray)
if pass {
resp.Link = links
_ = pack.IncDownCount(ctx)
return resp, nil
} else {
ApkReportErrorLinks(ctx, pack, links)
}
} else {
CheckChannelApkAutoGen(ctx, req.AppId, channelCode.DC, data, pack)
}
}
}
// 默认使用第一个
passedLinks := make([]string, 0)
for _, packageLink := range packageUrlArray {
link := strings.TrimSpace(packageLink + projectString + data.Md5)
checklink := link
cerr := CheckPackageLink(ctx, checklink)
if cerr != nil {
// log.Error("AppVersion", log.Any("link", link), log.E(cerr))
// if statg.Conf.RunMod == consts.RunModRelease && !slices.Contains([]int32{666, 998}, req.AppId) {
// appDict, _ := appInfoMod.GetAllAppIdNameMap(ctx)
// text := fmt.Sprintf("【%s】Android 下载链接异常:\n链接:%s\n错误:%s\n", appDict[req.AppId], link, cerr.Error())
// SendLinkErrorMessageToBot(text)
// }
} else {
passedLinks = append(passedLinks, link)
}
}
if len(passedLinks) > 0 {
resp.Link = passedLinks
}
} else {
link := strings.TrimSpace(packageUrlString + projectString + data.Md5)
resp.Link = []string{link}
}
}
}
return
}
// PageButtonList 落地页获取下载按钮
func PageButtonList(ctx context.Context, isIos bool, peerIP string, req *proto.PageButtonListReq) (resp *proto.PageButtonListResp, err error) {
resp = &proto.PageButtonListResp{
IosTF: false,
}
var channelCode ChannelParams
// TODO 存储记录
_ = json.Unmarshal([]byte(req.Channel), &channelCode)
channelCode.DC = utils.RepairDC(channelCode.DC)
req.Ip = peerIP
if req.Ip != "" {
logInfo := &downloadCpc.DownloadCpc{
AppId: req.AppId,
IP: req.Ip,
PC: channelCode.PC,
DC: channelCode.DC,
CH: channelCode.CH,
PeerIP: peerIP,
CreatedAt: time.Now(),
}
if landPageVisit.TryLock(req.Ip, time.Now().Add(landPageVisitIpTimeout)) {
cacheSer.Writers.LandPageVisitWriter.Add(logInfo)
}
}
cacheKey := fmt.Sprintf("version_button_list#appId:%d", req.AppId)
var buttons versionMod.VersionList
if cache, exists := cacheSer.Cache.Get(cacheKey); exists {
if list, ok := cache.(versionMod.VersionList); ok {
buttons = list
}
}
if buttons == nil {
buttons, err = versionMod.ButtonList(ctx, req.AppId)
if err != nil {
err = errors.New("服务器内部错误")
return
}
if len(buttons) > 0 {
cacheSer.Cache.Set(cacheKey, buttons, 0)
}
}
if len(buttons) < 1 {
err = errors.New("请开启版本")
return
}
for _, value := range buttons {
if isIos && value.Terminal == "android" {
// 如果是ios设备请求,记录是android 跳过
continue
}
if value.PackageType == versionMod.Enterprise && value.Terminal == "ios" {
resp.IosDown = true
}
if value.PackageType == versionMod.Shop && value.Terminal == "ios" {
resp.IosStore = true
if len(value.Link) > 0 {
resp.IosLink = value.Link[0]
}
}
if value.PackageType == versionMod.Tf && value.Terminal == "ios" {
resp.IosTF = true
}
if value.PackageType == versionMod.Light && value.Terminal == "ios" {
resp.IosH5 = true
}
}
// android下载按钮
packageUrlString := statg.Conf.URL.PackageUrl
packageUrlArray := strings.Split(packageUrlString, ";")
resp.AndroidButton = len(packageUrlArray)
// 是否有渠道包
var version *versionMod.VersionModel
versionKey := fmt.Sprintf("version_button#appId:%d#%s#%d", req.AppId, "android", 0)
if cache, exists := cacheSer.Cache.Get(versionKey); exists {
if item, ok := cache.(*versionMod.VersionModel); ok {
version = item
}
}
if version == nil {
version, err = versionMod.ButtonClick(ctx, req.AppId, "android", 0)
if err != nil {
return nil, err
}
cacheSer.Cache.Set(versionKey, version, 2*time.Minute)
}
pack, serr := channelpackage.FindOneChannelPackage(ctx, bson.M{
channelpackage.FN.ChannelPackage.AppId: req.AppId,
channelpackage.FN.ChannelPackage.Channel: channelCode.DC,
channelpackage.FN.ChannelPackage.Version: version.VersionNum,
channelpackage.FN.ChannelPackage.FileName: version.Md5,
})
if serr != nil && serr.Code != stdLog.DataNotExist {
return nil, errors.New(serr.Msg)
}
if pack != nil && pack.Status == channelpackage.ChanApkStatusSuccess {
resp.HasChannelApk = true
}
return
}