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 }