@@ -0,0 +1,172 @@
|
||||
package versionser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/localcache"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/commod"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
const (
|
||||
versionCacheTTL = time.Minute
|
||||
versionFailureCacheTTL = 10 * time.Second
|
||||
versionLastSuccessTTL = 24 * time.Hour
|
||||
)
|
||||
|
||||
var versionRequestGroup singleflight.Group
|
||||
|
||||
type PacketType int32
|
||||
|
||||
const (
|
||||
PacketTypeEnterprise PacketType = 1 // 企业签
|
||||
PacketTypeShop PacketType = 2 // 商店包
|
||||
PacketTypeTestFlight PacketType = 3 // TF包
|
||||
)
|
||||
|
||||
type AppAllReq struct {
|
||||
AppId int32 `json:"appId" binding:"required"` // AppID
|
||||
SysType string `json:"sysType,omitempty"` // 终端类型
|
||||
PktType PacketType `json:"pktType,omitempty"` // 包类型
|
||||
CurVersion string `json:"curVersion,omitempty"` // 当前版本
|
||||
CurPackageName string `json:"curPackageName,omitempty"` // 包名
|
||||
}
|
||||
|
||||
// 返回参数
|
||||
type AppAllResp struct {
|
||||
VersionInfo VersionInfo `json:"versionInfo"` // 版本信息
|
||||
AdvList []AdvertiseInfo `json:"advList"` // 广告信息
|
||||
AnnouList []AnnouInfo `json:"annouList"` // 公告信息
|
||||
IosUrl string `json:"iosLink"` // ios下载链接,网页版使用
|
||||
AndroidUrl string `json:"andLink"` // 安卓下载链接,网页版使用
|
||||
ShopIosLink string `json:"shopIosLink"` // ios商店包下载链接
|
||||
}
|
||||
|
||||
// 版本信息
|
||||
type VersionInfo struct {
|
||||
HasNewVersion bool `json:"hasNewVersion,omitempty"` // 是否存在新版本
|
||||
ServerVersion string `json:"serverVersion,omitempty"` // 服务器新版本
|
||||
DownloadLink []string `json:"downloadLink,omitempty"` // 下载地址
|
||||
Description string `json:"description,omitempty"` // 描述
|
||||
IsForceUpdate bool `json:"isForceUpdate,omitempty"` // 是否强制升级
|
||||
Size string `json:"size,omitempty"` // 大小
|
||||
Md5 string `json:"md5,omitempty"` // md5
|
||||
}
|
||||
|
||||
// 广告信息
|
||||
type AdvertiseInfo struct {
|
||||
Id int64 `json:"id"` // 广告id
|
||||
Title string `json:"title"` // 广告标题
|
||||
CoverImg string `json:"coverImg"` // 封面
|
||||
RandomImgs []string `json:"randomImgs"` // 随机封面
|
||||
RandomNames []string `json:"randomNames"` // 随机名称
|
||||
LocId int32 `json:"locId"` // 位置id
|
||||
JumpType int32 `json:"jumpType"` // 跳转方式 0:外部浏览器跳转 1:内部浏览器跳转 2:app内部跳转
|
||||
Link string `json:"link"` // 链接地址
|
||||
Sort int64 `json:"sort"` // 排序
|
||||
Duration int32 `json:"duration"` // 广告持续时间(弃用)
|
||||
CoverImgSize string `json:"coverImgSize"` // 封面尺寸
|
||||
WatchTime int `json:"watchTime"` // 视频广告时长
|
||||
}
|
||||
|
||||
// 公告信息
|
||||
type AnnouInfo struct {
|
||||
//id
|
||||
ID string `json:"id"`
|
||||
//名字
|
||||
Title string `json:"title"`
|
||||
//内容
|
||||
Content string `json:"content"`
|
||||
//广告图
|
||||
Cover string `json:"cover"`
|
||||
//链接地址
|
||||
Href string `json:"href"`
|
||||
//类型
|
||||
Type int64 `json:"type"`
|
||||
//时间
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
// 广告、版本、公告,三合一接口
|
||||
func AdvVersionAnnounThreeServer(curVersion, sysType string) (ver VersionInfo, adv []AdvertiseInfo, annou []AnnouInfo, iosUrl, androidUrl, shopIosLink string, err error) {
|
||||
//版本
|
||||
key := "VerAnnInfo:" + sysType + ":" + curVersion
|
||||
if resp, ok := getVersionCache(key); ok {
|
||||
ver, annou, iosUrl, androidUrl, shopIosLink = unpackAppAllResp(resp)
|
||||
return
|
||||
}
|
||||
|
||||
value, requestErr, _ := versionRequestGroup.Do(key, func() (interface{}, error) {
|
||||
// 同一版本的并发请求只允许一个访问产品中心;等待者进入后再检查一次缓存。
|
||||
if cached, ok := getVersionCache(key); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
resp, fetchErr := fetchAppAllResp(curVersion, sysType)
|
||||
if fetchErr == nil {
|
||||
localcache.C.Set(key, resp, versionCacheTTL)
|
||||
localcache.C.Set(key+":lastSuccess", resp, versionLastSuccessTTL)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 下游异常时短暂缓存最后一次成功结果(没有则为空结果),避免每个 Ping
|
||||
// 都同步等待同一个失败请求;10 秒后自动重试,恢复后能快速拿到新数据。
|
||||
fallback, _ := getVersionCache(key + ":lastSuccess")
|
||||
localcache.C.Set(key, fallback, versionFailureCacheTTL)
|
||||
return fallback, fetchErr
|
||||
})
|
||||
resp, ok := value.(AppAllResp)
|
||||
if !ok {
|
||||
return ver, adv, annou, iosUrl, androidUrl, shopIosLink, fmt.Errorf("invalid version cache type")
|
||||
}
|
||||
ver, annou, iosUrl, androidUrl, shopIosLink = unpackAppAllResp(resp)
|
||||
err = requestErr
|
||||
//log.Info("AdvVersionAnnounThreeServer response", log.Any("resp", resp))
|
||||
return
|
||||
}
|
||||
|
||||
func getVersionCache(key string) (AppAllResp, bool) {
|
||||
value, ok := localcache.C.Get(key)
|
||||
if !ok || value == nil {
|
||||
return AppAllResp{}, false
|
||||
}
|
||||
resp, ok := value.(AppAllResp)
|
||||
return resp, ok
|
||||
}
|
||||
|
||||
func unpackAppAllResp(resp AppAllResp) (VersionInfo, []AnnouInfo, string, string, string) {
|
||||
return resp.VersionInfo, resp.AnnouList, resp.IosUrl, resp.AndroidUrl, resp.ShopIosLink
|
||||
}
|
||||
|
||||
func fetchAppAllResp(curVersion, sysType string) (AppAllResp, error) {
|
||||
req := AppAllReq{
|
||||
AppId: commod.KFK_APPID,
|
||||
PktType: PacketTypeEnterprise,
|
||||
SysType: sysType,
|
||||
CurVersion: curVersion,
|
||||
}
|
||||
bodyStr, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("AdvVersionAnnounThreeServer json.Marshal is fail error:%+v/data:%+v", err, req))
|
||||
return AppAllResp{}, err
|
||||
}
|
||||
|
||||
url := appg.Conf.URL.ProductUrl + "/api/stat/all/app"
|
||||
resp := AppAllResp{}
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr)
|
||||
if err != nil {
|
||||
log.Error("AdvVersionAnnounThreeServer POSTWithJResp ", log.Any("url", url), log.E(err))
|
||||
return AppAllResp{}, err
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("AdvVersionAnnounThreeServer response status ", log.Any("code", code))
|
||||
return AppAllResp{}, fmt.Errorf("response status %d", code)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package versionser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/localcache"
|
||||
)
|
||||
|
||||
func TestAdvVersionAnnounThreeServerCoalescesConcurrentMisses(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
var badRequest atomic.Bool
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls.Add(1)
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/api/stat/all/app" {
|
||||
badRequest.Store(true)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
return
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
_ = json.NewEncoder(w).Encode(AppAllResp{
|
||||
VersionInfo: VersionInfo{ServerVersion: "9.9.9"},
|
||||
AnnouList: []AnnouInfo{{ID: "announcement-1"}},
|
||||
IosUrl: "ios-url",
|
||||
AndroidUrl: "android-url",
|
||||
ShopIosLink: "shop-ios-url",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
setVersionTestConfig(t, server.URL)
|
||||
version := "singleflight-test"
|
||||
deleteVersionTestCache(t, "VerAnnInfo:"+version)
|
||||
|
||||
const concurrency = 20
|
||||
start := make(chan struct{})
|
||||
errs := make(chan error, concurrency)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-start
|
||||
ver, _, annou, iosURL, androidURL, shopURL, err := AdvVersionAnnounThreeServer(version, "android")
|
||||
if err == nil && (ver.ServerVersion != "9.9.9" || len(annou) != 1 || iosURL != "ios-url" || androidURL != "android-url" || shopURL != "shop-ios-url") {
|
||||
err = &unexpectedVersionResponseError{}
|
||||
}
|
||||
errs <- err
|
||||
}()
|
||||
}
|
||||
close(start)
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
for err := range errs {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected result: %v", err)
|
||||
}
|
||||
}
|
||||
if got := calls.Load(); got != 1 {
|
||||
t.Fatalf("downstream calls = %d, want 1", got)
|
||||
}
|
||||
if badRequest.Load() {
|
||||
t.Fatal("downstream request used an unexpected method or path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdvVersionAnnounThreeServerCachesFailureFallback(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
calls.Add(1)
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
setVersionTestConfig(t, server.URL)
|
||||
version := "failure-cache-test"
|
||||
deleteVersionTestCache(t, "VerAnnInfo:"+version)
|
||||
|
||||
if _, _, _, _, _, _, err := AdvVersionAnnounThreeServer(version, "android"); err == nil {
|
||||
t.Fatal("first request error = nil, want downstream status error")
|
||||
}
|
||||
if _, _, _, _, _, _, err := AdvVersionAnnounThreeServer(version, "android"); err != nil {
|
||||
t.Fatalf("cached fallback error = %v, want nil", err)
|
||||
}
|
||||
if got := calls.Load(); got != 1 {
|
||||
t.Fatalf("downstream calls = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
type unexpectedVersionResponseError struct{}
|
||||
|
||||
func (*unexpectedVersionResponseError) Error() string { return "unexpected version response" }
|
||||
|
||||
func setVersionTestConfig(t *testing.T, productURL string) {
|
||||
t.Helper()
|
||||
previous := appg.Conf
|
||||
conf := &appg.GlobalConfig{}
|
||||
conf.URL.ProductUrl = productURL
|
||||
appg.Conf = conf
|
||||
t.Cleanup(func() { appg.Conf = previous })
|
||||
}
|
||||
|
||||
func deleteVersionTestCache(t *testing.T, key string) {
|
||||
t.Helper()
|
||||
localcache.C.Delete(key)
|
||||
localcache.C.Delete(key + ":lastSuccess")
|
||||
t.Cleanup(func() {
|
||||
localcache.C.Delete(key)
|
||||
localcache.C.Delete(key + ":lastSuccess")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package versionser
|
||||
|
||||
import (
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/crypt"
|
||||
"91porn-server/common/version"
|
||||
"91porn-server/models/v/versionmod"
|
||||
)
|
||||
|
||||
func HandleVersionForList(ver, sysType string) (verlist []*versionmod.VersionBody) {
|
||||
v1, err := version.New(ver)
|
||||
if v1 == nil || err != nil {
|
||||
return
|
||||
}
|
||||
sys := common.HandleSysType(sysType)
|
||||
//首次查看当前版本有没有开启 提升,如果有指定的版本,则返回指定的版本,如果没有指定版本,则返回该平台最新的版本,如果没有打开则按照正常流程走
|
||||
vermod, err := versionmod.FindOneVersionByPlatVer(sys, ver)
|
||||
if vermod != nil && err == nil {
|
||||
if vermod.IsUpgrade {
|
||||
if vermod.SpecVersion != "" {
|
||||
newVersion, _ := versionmod.FindOneVersionByPlatVer(sys, vermod.SpecVersion)
|
||||
if newVersion != nil {
|
||||
verlist = append(verlist, &versionmod.VersionBody{
|
||||
Code: newVersion.Code,
|
||||
VersionName: newVersion.VersionName,
|
||||
Platform: newVersion.Platform,
|
||||
Description: newVersion.Description,
|
||||
ForcedUpdate: true,
|
||||
URL: newVersion.URL})
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
newVer, _ := versionmod.FindVersion(sys)
|
||||
verlist = append(verlist, &versionmod.VersionBody{
|
||||
Code: newVer.Code,
|
||||
VersionName: newVer.VersionName,
|
||||
Platform: newVer.Platform,
|
||||
Description: newVer.Description,
|
||||
ForcedUpdate: true,
|
||||
URL: newVer.URL})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
newVer, err := versionmod.FindVersion(sys)
|
||||
if newVer.VersionName == "" || err != nil {
|
||||
return
|
||||
}
|
||||
v2, _ := version.New(newVer.VersionName)
|
||||
if v1.GTE(v2) {
|
||||
return
|
||||
}
|
||||
if newVer.VersionRange.Major != "" {
|
||||
major, _ := version.New(newVer.VersionRange.Major)
|
||||
if v1.GT(major) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if newVer.VersionRange.Minor != "" {
|
||||
minor, _ := version.New(newVer.VersionRange.Minor)
|
||||
if v1.LT(minor) {
|
||||
return
|
||||
}
|
||||
}
|
||||
verlist = append(verlist, &versionmod.VersionBody{
|
||||
Code: newVer.Code,
|
||||
VersionName: newVer.VersionName,
|
||||
Platform: newVer.Platform,
|
||||
Description: newVer.Description,
|
||||
ForcedUpdate: newVer.ForcedUpdate,
|
||||
URL: newVer.URL})
|
||||
return
|
||||
}
|
||||
|
||||
// 当前版本是否在更新范围
|
||||
func HandleVersion(ver string, vermod *versionmod.Version) bool {
|
||||
v1, err := version.New(ver)
|
||||
if v1 == nil || err != nil {
|
||||
return false
|
||||
}
|
||||
if vermod != nil {
|
||||
if vermod.VersionRange.Major == "" && vermod.VersionRange.Minor == "" {
|
||||
return true
|
||||
}
|
||||
if vermod.VersionRange.Major != "" {
|
||||
major, _ := version.New(vermod.VersionRange.Major)
|
||||
if v1.GT(major) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if vermod.VersionRange.Minor != "" {
|
||||
minor, _ := version.New(vermod.VersionRange.Minor)
|
||||
if v1.LT(minor) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func CheckVersionBaseOnBuildId(ver, sysType, buildId string) (versionBody []*versionmod.VersionBody) {
|
||||
v1, err := version.New(ver)
|
||||
if v1 == nil || err != nil {
|
||||
return
|
||||
}
|
||||
sys := common.HandleSysType(sysType)
|
||||
//查看当前版本有没有开启 提升,如果有指定的版本,则返回指定的版本,如果没有指定版本,则返回该平台最新的版本,如果没有打开则按照正常流程走
|
||||
vermod, err := versionmod.FindOneVersionByPlatVerBuild(sys, ver, buildId)
|
||||
if vermod != nil && err == nil {
|
||||
if vermod.IsUpgrade {
|
||||
if vermod.SpecVersion != "" {
|
||||
newVersion, _ := versionmod.FindOneVersionByPlatVerBuild(sys, vermod.SpecVersion, buildId)
|
||||
if newVersion != nil {
|
||||
versionBody = append(versionBody, &versionmod.VersionBody{
|
||||
Code: newVersion.Code,
|
||||
VersionName: newVersion.VersionName,
|
||||
Platform: newVersion.Platform,
|
||||
Description: newVersion.Description,
|
||||
ForcedUpdate: true,
|
||||
URL: newVersion.URL})
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//分为带buildId的包 和不带buildId的包
|
||||
ver1, err := versionmod.FindVersionBaseOnBuildIdAndPlatForm(buildId, sys)
|
||||
if ver1 == nil || err != nil {
|
||||
return
|
||||
}
|
||||
if ver1.Origin == versionmod.TF {
|
||||
ver2, err := versionmod.FindVersionBaseOnBuildId(buildId)
|
||||
if ver2 == nil || err != nil {
|
||||
return
|
||||
}
|
||||
v2, _ := version.New(ver2.VersionName)
|
||||
if v1.GTE(v2) {
|
||||
return
|
||||
}
|
||||
versionBody = append(versionBody, &versionmod.VersionBody{
|
||||
Code: ver2.Code,
|
||||
VersionName: ver2.VersionName,
|
||||
Platform: ver2.Platform,
|
||||
Description: ver2.Description,
|
||||
ForcedUpdate: ver2.ForcedUpdate,
|
||||
URL: GetTFUrl(ver2.URL)})
|
||||
} else {
|
||||
versionBody = append(versionBody, &versionmod.VersionBody{
|
||||
Code: ver1.Code,
|
||||
VersionName: ver1.VersionName,
|
||||
Platform: ver1.Platform,
|
||||
Description: ver1.Description,
|
||||
ForcedUpdate: ver1.ForcedUpdate,
|
||||
URL: ver1.URL})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetTFUrl(tf string) string {
|
||||
if tf != "" {
|
||||
type tfStc struct {
|
||||
Url string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
var tfArr []tfStc
|
||||
_ = crypt.JSON2Struct(tf, &tfArr)
|
||||
tfArrLen := len(tfArr)
|
||||
if tfArrLen == 0 {
|
||||
return tf
|
||||
}
|
||||
weight := 100 / tfArrLen
|
||||
chioce := make([]common.Choice, tfArrLen)
|
||||
for i := 0; i < len(tfArr); i++ {
|
||||
chioce[i] = common.Choice{
|
||||
Weight: weight,
|
||||
Item: tfArr[i].Url,
|
||||
}
|
||||
}
|
||||
ch, _ := common.WeightedChoice(chioce)
|
||||
if v, ok := ch.Item.(string); ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return tf
|
||||
}
|
||||
Reference in New Issue
Block a user