2025-12-06 10:38:48 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2025-12-06 15:24:30 +08:00
|
|
|
|
"cidservice/dao"
|
|
|
|
|
|
"cidservice/model/dto"
|
|
|
|
|
|
"cidservice/model/entity"
|
|
|
|
|
|
"cidservice/model/types"
|
2025-12-06 10:38:48 +08:00
|
|
|
|
"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 = cidService{}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type cidService struct{}
|
|
|
|
|
|
|
|
|
|
|
|
// AdMatchingStrategy 广告匹配策略
|
|
|
|
|
|
type AdMatchingStrategy struct {
|
|
|
|
|
|
TenantLevel string // 租户级别
|
|
|
|
|
|
MinConversion float64 // 最低转化率
|
|
|
|
|
|
MaxConversion float64 // 最高转化率
|
|
|
|
|
|
SourceWeight map[string]int // 广告源权重
|
|
|
|
|
|
MaxAdsPerRequest int // 每次请求最大广告数
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getMatchingStrategy 获取匹配策略
|
|
|
|
|
|
func (s *cidService) 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 *cidService) 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, "获取租户信息失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 15:24:30 +08:00
|
|
|
|
// 检查租户请求次数限制
|
|
|
|
|
|
allowed, err := RateLimit.CheckTenantRequestLimit(ctx, tenant.Id, nil)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, gerror.Wrap(err, "检查租户请求限制失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
if !allowed {
|
|
|
|
|
|
return nil, gerror.New("租户请求次数已超过限制,请稍后再试")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 10:38:48 +08:00
|
|
|
|
// 获取匹配策略
|
|
|
|
|
|
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 *cidService) 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 *cidService) 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 *cidService) 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 *cidService) 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 *cidService) 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 *cidService) 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 *cidService) generateUniqueCID() string {
|
|
|
|
|
|
timestamp := time.Now().Unix()
|
|
|
|
|
|
random := rand.Intn(8999) + 1000
|
|
|
|
|
|
return fmt.Sprintf("CID_%d_%d", timestamp, random)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// recordCIDRequest 记录CID请求
|
|
|
|
|
|
func (s *cidService) 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 *cidService) 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
|
|
|
|
|
|
}
|