package service import ( "cid/dao" "cid/model/dto" "cid/model/entity" "cid/model/types" "context" "encoding/json" "fmt" "math/rand" "strconv" "time" "gitee.com/red-future---jilin-g/common/utils" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) var ( CID = cid{} ) type cid struct{} // AdMatchingStrategy 广告匹配策略 type AdMatchingStrategy struct { TenantLevel string // 租户级别 MinConversion float64 // 最低转化率 MaxConversion float64 // 最高转化率 SourceWeight map[string]int // 广告源权重 MaxAdsPerRequest int // 每次请求最大广告数 } // getMatchingStrategy 获取匹配策略 func (s *cid) getMatchingStrategy(ctx context.Context, tenantLevel string) (*AdMatchingStrategy, error) { // 从数据库获取策略 strategyEntity, err := Strategy.GetStrategyByTenantLevel(ctx, tenantLevel) if err != nil { return nil, err } if strategyEntity == nil { // 返回默认策略 return &AdMatchingStrategy{ TenantLevel: tenantLevel, MinConversion: 0.01, MaxConversion: 0.05, SourceWeight: map[string]int{"self": 100}, MaxAdsPerRequest: 3, }, nil } // 反序列化权重配置 var sourceWeights map[string]int if strategyEntity.SourceWeights != "" { err = json.Unmarshal([]byte(strategyEntity.SourceWeights), &sourceWeights) if err != nil { g.Log().Warningf(ctx, "策略权重反序列化失败: %v", err) sourceWeights = map[string]int{"self": 100} } } return &AdMatchingStrategy{ TenantLevel: strategyEntity.TenantLevel, MinConversion: strategyEntity.MinConversion, MaxConversion: strategyEntity.MaxConversion, SourceWeight: sourceWeights, MaxAdsPerRequest: strategyEntity.MaxAdsPerReq, }, nil } // GenerateCID 生成CID广告 func (s *cid) GenerateCID(ctx context.Context, req *dto.GenerateCIDReq) (res *dto.GenerateCIDRes, err error) { // 获取当前用户信息 userInfo, err := utils.GetUserInfo(ctx) if err != nil { return nil, gerror.Wrap(err, "获取用户信息失败") } // 获取租户信息 tenant, err := s.getTenantByUser(ctx, gconv.Int64(userInfo.UserName)) if err != nil { return nil, gerror.Wrap(err, "获取租户信息失败") } // 检查租户请求次数限制 allowed, err := RateLimit.CheckTenantRequestLimit(ctx, tenant.Id, nil) if err != nil { return nil, gerror.Wrap(err, "检查租户请求限制失败") } if !allowed { return nil, gerror.New("租户请求次数已超过限制,请稍后再试") } // 获取匹配策略 strategy, err := s.getMatchingStrategy(ctx, tenant.Level) if err != nil { return nil, gerror.Wrap(err, "获取匹配策略失败") } // 根据策略获取广告 ads, err := s.matchAds(ctx, req, strategy) if err != nil { return nil, gerror.Wrap(err, "广告匹配失败") } // 记录CID请求 go s.recordCIDRequest(context.Background(), req, tenant, ads) // 生成唯一CID cid := s.generateUniqueCID() return &dto.GenerateCIDRes{ CID: cid, Ads: ads, TotalAds: len(ads), TenantId: tenant.Id, TenantName: tenant.Name, GeneratedAt: time.Now().Format("2006-01-02 15:04:05"), }, nil } // getTenantByUser 根据用户获取租户信息 func (s *cid) getTenantByUser(ctx context.Context, userId int64) (*types.Tenant, error) { // 通过common模块获取用户信息,包含租户ID userInfo, err := utils.GetUserInfo(ctx) if err != nil { return nil, gerror.Wrap(err, "获取用户信息失败") } // 租户ID从用户信息中获取 tenantId := gconv.Int64(userInfo.TenantId) if tenantId == 0 { tenantId = 1 // 默认租户ID } // 租户级别和名称可以根据租户ID通过其他方式获取或配置 // 这里使用映射配置,实际项目中可能需要调用其他服务 tenantName := "默认租户" tenantLevel := "basic" tenantStatus := "active" // 根据租户ID设置不同的级别(示例逻辑) switch tenantId { case 1: tenantName = "基础租户" tenantLevel = "basic" case 2: tenantName = "标准租户" tenantLevel = "standard" case 3: tenantName = "高级租户" tenantLevel = "premium" } return &types.Tenant{ Id: tenantId, Name: tenantName, Level: tenantLevel, Status: tenantStatus, }, nil } // matchAds 根据策略匹配广告 func (s *cid) matchAds(ctx context.Context, req *dto.GenerateCIDReq, strategy *AdMatchingStrategy) ([]*dto.AdInfo, error) { var matchedAds []*dto.AdInfo // 根据策略权重从不同源获取广告 for source, weight := range strategy.SourceWeight { if weight <= 0 { continue } sourceAds, err := s.getAdsFromSource(ctx, source, req, strategy, weight) if err != nil { g.Log().Warningf(ctx, "从广告源 %s 获取广告失败: %v", source, err) continue } matchedAds = append(matchedAds, sourceAds...) } // 过滤符合转化率要求的广告 var filteredAds []*dto.AdInfo for _, ad := range matchedAds { if ad.ConversionRate >= strategy.MinConversion && ad.ConversionRate <= strategy.MaxConversion { filteredAds = append(filteredAds, ad) } } // 限制广告数量 if len(filteredAds) > strategy.MaxAdsPerRequest { rand.Shuffle(len(filteredAds), func(i, j int) { filteredAds[i], filteredAds[j] = filteredAds[j], filteredAds[i] }) filteredAds = filteredAds[:strategy.MaxAdsPerRequest] } return filteredAds, nil } // getAdsFromSource 从指定广告源获取广告 func (s *cid) getAdsFromSource(ctx context.Context, source string, req *dto.GenerateCIDReq, strategy *AdMatchingStrategy, weight int) ([]*dto.AdInfo, error) { switch source { case "self": return s.getSelfServiceAds(ctx, req, weight) case "google": return s.getGoogleAds(ctx, req, weight) case "facebook": return s.getFacebookAds(ctx, req, weight) default: return nil, gerror.Newf("不支持的广告源: %s", source) } } // getSelfServiceAds 获取自营广告 func (s *cid) getSelfServiceAds(ctx context.Context, req *dto.GenerateCIDReq, count int) ([]*dto.AdInfo, error) { // 这里应该从数据库查询自营广告 // 暂时返回模拟数据 ads := make([]*dto.AdInfo, 0) for i := 0; i < count; i++ { ads = append(ads, &dto.AdInfo{ Id: int64(rand.Intn(89999) + 10000), Title: fmt.Sprintf("自营广告 %d", i+1), Description: "这是一个高质量的自营广告", ImageUrl: "https://example.com/ad.jpg", TargetUrl: "https://example.com/landing", ConversionRate: rand.Float64(), Source: "self", Bid: rand.Intn(901) + 100, }) } return ads, nil } // getGoogleAds 获取Google广告 func (s *cid) getGoogleAds(ctx context.Context, req *dto.GenerateCIDReq, count int) ([]*dto.AdInfo, error) { // 这里应该调用Google Ads API // 暂时返回模拟数据 ads := make([]*dto.AdInfo, 0) for i := 0; i < count; i++ { ads = append(ads, &dto.AdInfo{ Id: int64(rand.Intn(9999) + 20000), Title: fmt.Sprintf("Google广告 %d", i+1), Description: "来自Google的高质量广告", ImageUrl: "https://google.com/ad.jpg", TargetUrl: "https://google.com/landing", ConversionRate: rand.Float64()*0.3 + 0.1, Source: "google", Bid: rand.Intn(1301) + 200, }) } return ads, nil } // getFacebookAds 获取Facebook广告 func (s *cid) getFacebookAds(ctx context.Context, req *dto.GenerateCIDReq, count int) ([]*dto.AdInfo, error) { // 这里应该调用Facebook Ads API // 暂时返回模拟数据 ads := make([]*dto.AdInfo, 0) for i := 0; i < count; i++ { ads = append(ads, &dto.AdInfo{ Id: int64(rand.Intn(9999) + 30000), Title: fmt.Sprintf("Facebook广告 %d", i+1), Description: "来自Facebook的高质量广告", ImageUrl: "https://facebook.com/ad.jpg", TargetUrl: "https://facebook.com/landing", ConversionRate: rand.Float64()*0.25 + 0.08, Source: "facebook", Bid: rand.Intn(1051) + 150, }) } return ads, nil } // generateUniqueCID 生成唯一CID func (s *cid) generateUniqueCID() string { timestamp := time.Now().Unix() random := rand.Intn(8999) + 1000 return fmt.Sprintf("CID_%d_%d", timestamp, random) } // recordCIDRequest 记录CID请求 func (s *cid) recordCIDRequest(ctx context.Context, req *dto.GenerateCIDReq, tenant *types.Tenant, ads []*dto.AdInfo) { // 转换dto.AdInfo到entity.Ad var entityAds []entity.Ad for _, ad := range ads { entityAds = append(entityAds, entity.Ad{ ID: fmt.Sprintf("%d", ad.Id), AdSource: ad.Source, Title: ad.Title, Description: ad.Description, CreativeURL: ad.ImageUrl, LandingURL: ad.TargetUrl, BidAmount: int64(ad.Bid), }) } request := &entity.CidRequest{ RequestID: fmt.Sprintf("REQ_%d_%d", time.Now().Unix(), rand.Intn(10000)), UserID: fmt.Sprintf("%d", req.UserId), TenantID: fmt.Sprintf("%d", tenant.Id), Response: &entity.CidResponse{ Ads: entityAds, }, ProcessingTime: int64(rand.Intn(401) + 100), // 模拟处理时间 } dao.CIDRequest.Create(ctx, request) } // GetCIDHistory 获取CID请求历史 func (s *cid) GetCIDHistory(ctx context.Context, userId int64, page, size int) (res *dto.GetCIDHistoryRes, err error) { history, total, err := dao.CIDRequest.GetHistory(ctx, userId, page, size) if err != nil { return nil, err } var historyList []*dto.CIDRequestHistory for _, record := range history { // 解析TenantID tenantId := int64(0) if record.TenantID != "" { tenantId, _ = strconv.ParseInt(record.TenantID, 10, 64) } // 解析UserID uid := int64(0) if record.UserID != "" { uid, _ = strconv.ParseInt(record.UserID, 10, 64) } historyList = append(historyList, &dto.CIDRequestHistory{ Id: 0, // 使用默认值,因为entity使用的是ObjectID TenantId: tenantId, UserId: uid, RequestType: "CID", // 默认值 Status: "completed", // 从response状态获取 ProcessTime: int(record.ProcessingTime), CreatedAt: record.CreatedAt.String(), }) } return &dto.GetCIDHistoryRes{ List: historyList, Total: total, Page: page, Size: size, }, nil }