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 }