Files
cid/service/cid_service.go
2026-02-24 16:24:47 +08:00

378 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"cid/dao"
"cid/model/dto"
"cid/model/entity"
"context"
"encoding/json"
"fmt"
"math/rand"
"strconv"
"time"
"gitea.com/red-future/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: tenantLevel, // 使用传入的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, "获取租户信息失败")
}
// 检查租户请求次数限制
// 将租户ID转换为int64用于限流检查
tenantIdInt := int64(0)
if tenant.Id != "" && tenant.Id != "default" {
tryInt, _ := strconv.ParseInt(tenant.Id, 10, 64)
tenantIdInt = tryInt
}
allowed, err := RateLimit.CheckTenantRequestLimit(ctx, tenantIdInt, 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()
// 转换租户ID为int64兼容性处理
// 这里直接使用之前已经声明的tenantIdInt变量不需要重新声明
if tenant.Id != "" && tenant.Id != "default" {
// 这里简化处理,实际可能需要更复杂的转换逻辑
tryInt, parseErr := strconv.ParseInt(tenant.Id, 10, 64)
if parseErr == nil {
tenantIdInt = tryInt
}
}
return &dto.GenerateCIDRes{
CID: cid,
Ads: ads,
TotalAds: len(ads),
TenantId: tenantIdInt,
TenantName: tenant.Name,
GeneratedAt: time.Now().Format("2006-01-02 15:04:05"),
}, nil
}
// getTenantByUser 根据用户获取租户信息
func (s *cid) getTenantByUser(ctx context.Context, userId int64) (*dto.TenantInfo, error) {
// 通过common模块获取用户信息包含租户ID
userInfo, err := utils.GetUserInfo(ctx)
if err != nil {
return nil, gerror.Wrap(err, "获取用户信息失败")
}
// 租户ID直接从用户信息中获取
tenantId := ""
if userInfo.TenantId != nil {
if tenantIdStr, ok := userInfo.TenantId.(string); ok && tenantIdStr != "" {
tenantId = tenantIdStr
}
} else {
tenantId = "default" // 默认租户ID
tenantId = "default" // 默认租户ID
}
// 租户级别和名称可以根据租户ID通过其他方式获取或配置
// 这里使用映射配置,实际项目中可能需要调用其他服务
tenantName := "默认租户"
tenantLevel := "basic"
// 根据租户ID设置不同的级别示例逻辑
switch tenantId {
case "default":
tenantName = "基础租户"
tenantLevel = "basic"
case "standard":
tenantName = "标准租户"
tenantLevel = "standard"
case "premium":
tenantName = "高级租户"
tenantLevel = "premium"
default:
// 如果租户ID不是预设值使用租户ID作为名称
tenantName = tenantId + "租户"
tenantLevel = "basic"
}
return &dto.TenantInfo{
Id: tenantId,
Name: tenantName,
Level: tenantLevel,
}, 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 *dto.TenantInfo, 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),
Response: &entity.CidResponse{
Ads: entityAds,
},
ProcessingTime: int64(rand.Intn(401) + 100), // 模拟处理时间
}
_, err := dao.CIDRequest.Create(ctx, request)
if err != nil {
g.Log().Errorf(ctx, "记录CID请求失败: %v", err)
}
}
// 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, strconv.FormatInt(userId, 10), page, size)
if err != nil {
return nil, err
}
var historyList []*dto.CIDRequestHistory
for _, record := range history {
// 解析TenantID
tenantId := int64(0)
if tenantIdStr, ok := record.TenantId.(string); ok && tenantIdStr != "" {
tenantId, _ = strconv.ParseInt(tenantIdStr, 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
}