353 lines
10 KiB
Go
353 lines
10 KiB
Go
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
|
||
}
|