15 KiB
15 KiB
name, description, compatibility, metadata
| name | description | compatibility | metadata | ||||
|---|---|---|---|---|---|---|---|
| ads-development | Ads ecosystem patterns và development workflow. Use for campaign management, RTB engine, billing integration, tracking/attribution, và analytics. | .NET 10+, MediatR, EF Core, Redis, RabbitMQ, MassTransit |
|
Ads Development Skill / Phát Triển Hệ Thống Quảng Cáo
Patterns và workflows cho việc phát triển Ads Services ecosystem trong GoodGo.
When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Developing campaign management features / Phát triển tính năng quản lý chiến dịch
- Implementing RTB (Real-Time Bidding) / Triển khai đấu giá thời gian thực
- Working with ads billing/payment / Làm việc với thanh toán quảng cáo
- Building tracking/attribution / Xây dựng tracking và attribution
- Creating analytics dashboards / Tạo dashboard phân tích
Core Concepts / Khái Niệm Cốt Lõi
Ads Services Ecosystem
┌─────────────────────────────────────────────────────────────────────┐
│ ADS SERVICES ECOSYSTEM │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ ads-manager │ │ ads-serving │ │ ads-billing │ │
│ │ service │ │ service │ │ service │ │
│ │ │ │ │ │ │ │
│ │ • Campaigns │ │ • RTB Engine │ │ • Prepaid │ │
│ │ • Ad Sets │ │ • Ad Selection │ │ • Credit Lines │ │
│ │ • Ads │ │ • Pacing │ │ • Invoicing │ │
│ │ • Targeting │ │ • Frequency │ │ • Thresholds │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └──────────────┬─────┴────────────────────┘ │
│ │ │
│ ┌─────▼─────┐ │
│ │ RabbitMQ │ │
│ │ Event Bus │ │
│ └─────┬─────┘ │
│ │ │
│ ┌─────────────────┐ │ ┌─────────────────┐ │
│ │ ads-tracking │◄────┴────►│ ads-analytics │ │
│ │ service │ │ service │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Campaign 3-Tier Structure / Cấu Trúc 3 Cấp Độ
Campaign (Chiến dịch)
├── Objective: Awareness | Traffic | Conversion
├── Status: Draft | Active | Paused | Completed
└── Budget: Daily | Lifetime
│
├── AdSet 1 (Nhóm quảng cáo)
│ ├── Targeting: Core | Interest | Custom | Lookalike
│ ├── Placement: Feed | Story | Search
│ ├── Schedule: Start/End dates
│ └── BidStrategy: CPC | CPM | OCPM
│ │
│ ├── Ad 1 (Mẫu quảng cáo)
│ │ ├── Creative: Image | Video | Carousel
│ │ ├── Headline: Text
│ │ └── CTA: Learn More | Shop Now | etc.
│ │
│ └── Ad 2
│
└── AdSet 2
Key Patterns / Mẫu Chính
Campaign Aggregate
/// <summary>
/// EN: Campaign aggregate root.
/// VI: Aggregate root cho Campaign.
/// </summary>
public class Campaign : Entity, IAggregateRoot
{
private readonly List<AdSet> _adSets = new();
public Guid AdvertiserId { get; private set; }
public string Name { get; private set; }
public CampaignObjective Objective { get; private set; }
public CampaignStatus Status { get; private set; }
public CampaignBudget Budget { get; private set; }
public IReadOnlyCollection<AdSet> AdSets => _adSets.AsReadOnly();
public Campaign(
Guid advertiserId,
string name,
CampaignObjective objective,
CampaignBudget budget)
{
AdvertiserId = advertiserId;
Name = Guard.Against.NullOrWhiteSpace(name, nameof(name));
Objective = objective;
Budget = budget;
Status = CampaignStatus.Draft;
AddDomainEvent(new CampaignCreatedDomainEvent(this));
}
public void Activate()
{
if (Status != CampaignStatus.Draft)
throw new AdsDomainException("Only draft campaigns can be activated");
if (!_adSets.Any(a => a.Ads.Any()))
throw new AdsDomainException("Campaign must have at least one ad");
Status = CampaignStatus.Active;
AddDomainEvent(new CampaignActivatedDomainEvent(this));
}
public void Pause() { /* ... */ }
}
/// <summary>
/// EN: Campaign objective value object.
/// VI: Value object cho mục tiêu chiến dịch.
/// </summary>
public record CampaignObjective
{
public static CampaignObjective Awareness => new("AWARENESS");
public static CampaignObjective Traffic => new("TRAFFIC");
public static CampaignObjective Conversion => new("CONVERSION");
public string Value { get; }
private CampaignObjective(string value) => Value = value;
}
RTB Auction Engine
/// <summary>
/// EN: Real-time bidding auction engine.
/// VI: Engine đấu giá thời gian thực.
/// </summary>
public class AuctionEngine
{
private readonly ICacheService _cache;
private readonly IFrequencyCapService _frequencyCap;
private readonly IBudgetPacer _budgetPacer;
public async Task<AuctionResult?> RunAuctionAsync(
AdRequest request,
CancellationToken ct = default)
{
// EN: 1. Get candidate ads from cache
// VI: 1. Lấy danh sách ads từ cache
var candidateAds = await _cache.GetAsync<List<CachedAd>>(
$"ads:active:{request.PlacementType}", ct);
if (candidateAds == null || !candidateAds.Any())
return null;
// EN: 2. Filter by targeting
// VI: 2. Lọc theo targeting
var eligibleAds = candidateAds
.Where(ad => MatchesTargeting(ad.Targeting, request.UserContext))
.ToList();
// EN: 3. Apply frequency cap
// VI: 3. Áp dụng frequency cap
eligibleAds = await _frequencyCap.FilterAsync(
eligibleAds, request.UserId, ct);
// EN: 4. Apply budget pacing
// VI: 4. Áp dụng pacing ngân sách
eligibleAds = await _budgetPacer.FilterAsync(eligibleAds, ct);
// EN: 5. Calculate eCPM and rank
// VI: 5. Tính eCPM và xếp hạng
var ranked = eligibleAds
.Select(ad => new Bid
{
Ad = ad,
eCPM = CalculateEcpm(ad, request)
})
.OrderByDescending(b => b.eCPM)
.ToList();
if (!ranked.Any())
return null;
// EN: 6. Select winner (second-price auction)
// VI: 6. Chọn winner (đấu giá giá thứ 2)
var winner = ranked.First();
var secondPrice = ranked.Count > 1
? ranked[1].eCPM
: winner.eCPM * 0.9m;
return new AuctionResult
{
WinningAd = winner.Ad,
FinalPrice = secondPrice + 0.01m
};
}
private decimal CalculateEcpm(CachedAd ad, AdRequest request)
{
// eCPM = (Bid × Predicted CTR × 1000) + Quality Score
var predictedCtr = PredictCtr(ad, request);
var qualityScore = CalculateQualityScore(ad);
return (ad.BidAmount * predictedCtr * 1000) + qualityScore;
}
}
Tracking Pixel Event
/// <summary>
/// EN: Pixel event handler.
/// VI: Handler cho pixel event.
/// </summary>
public class TrackPixelEventCommandHandler
: IRequestHandler<TrackPixelEventCommand, TrackingResult>
{
private readonly IConversionWindow _conversionWindow;
private readonly IAttributionService _attribution;
private readonly IPublishEndpoint _publisher;
public async Task<TrackingResult> Handle(
TrackPixelEventCommand request,
CancellationToken ct)
{
// EN: 1. Validate pixel belongs to advertiser
// VI: 1. Xác thực pixel thuộc về advertiser
var pixel = await _pixelRepository.GetAsync(request.PixelId, ct);
if (pixel == null)
return TrackingResult.InvalidPixel();
// EN: 2. Record event
// VI: 2. Ghi nhận event
var pixelEvent = new PixelEvent(
request.PixelId,
request.EventType,
request.EventData,
request.UserId,
DateTime.UtcNow);
await _eventRepository.AddAsync(pixelEvent, ct);
// EN: 3. Check for conversion attribution
// VI: 3. Kiểm tra attribution cho conversion
if (IsConversionEvent(request.EventType))
{
var attribution = await _attribution.FindAttributionAsync(
request.UserId,
_conversionWindow.GetWindow(pixel.Settings),
ct);
if (attribution != null)
{
await _publisher.Publish(new ConversionAttributedEvent
{
ConversionId = pixelEvent.Id,
AdId = attribution.AdId,
CampaignId = attribution.CampaignId,
Value = request.EventData.GetValueOrDefault("value", 0)
}, ct);
}
}
return TrackingResult.Success(pixelEvent.Id);
}
}
Integration Events
/// <summary>
/// EN: Ads ecosystem integration events.
/// VI: Integration events cho Ads ecosystem.
/// </summary>
// ads-manager → ads-serving
public record CampaignActivatedIntegrationEvent(
Guid CampaignId,
List<AdSetInfo> AdSets,
DateTime ActivatedAt) : IIntegrationEvent;
// ads-serving → ads-billing
public record AdImpressionChargedEvent(
Guid AdId,
Guid CampaignId,
Guid AdvertiserId,
decimal CostAmount,
string BillingModel, // CPC, CPM
DateTime ChargedAt) : IIntegrationEvent;
// ads-tracking → ads-analytics
public record ConversionAttributedEvent(
Guid ConversionId,
Guid AdId,
Guid CampaignId,
decimal ConversionValue,
string AttributionModel,
DateTime AttributedAt) : IIntegrationEvent;
// ads-billing → wallet-service (existing)
public record WalletDebitRequestedEvent(
Guid WalletId,
decimal Amount,
string Description,
Guid ReferenceId,
string ReferenceType) : IIntegrationEvent;
Common Mistakes / Lỗi Thường Gặp
1. Not Using Redis for Frequency Cap
// ❌ BAD: Database lookup for every request
var viewCount = await _dbContext.AdViews
.CountAsync(v => v.UserId == userId && v.AdId == adId);
// ✅ GOOD: Redis with atomic increment
var key = $"freq:{userId}:{adId}:{DateTime.UtcNow:yyyyMMdd}";
var count = await _redis.StringIncrementAsync(key);
if (count == 1)
await _redis.KeyExpireAsync(key, TimeSpan.FromDays(1));
2. Not Handling Budget Race Conditions
// ❌ BAD: Read-then-write race condition
var spent = await GetTodaySpend(campaignId);
if (spent + cost <= budget)
await RecordSpend(campaignId, cost);
// ✅ GOOD: Atomic increment with Lua script
var script = @"
local current = redis.call('GET', KEYS[1])
local budget = tonumber(ARGV[1])
local cost = tonumber(ARGV[2])
current = current and tonumber(current) or 0
if current + cost <= budget then
redis.call('INCRBYFLOAT', KEYS[1], cost)
return 1
end
return 0
";
var result = await _redis.ScriptEvaluateAsync(script, keys, args);
3. Blocking on Analytics Write
// ❌ BAD: Sync write blocks ad serving
await _analyticsDb.InsertAsync(impression);
return adResponse;
// ✅ GOOD: Async publish, return immediately
await _publisher.Publish(new ImpressionEvent(impression));
return adResponse; // < 100ms
Quick Reference / Tham Chiếu Nhanh
Service Responsibilities
| Service | Primary Responsibility |
|---|---|
| ads-manager | Campaign CRUD, targeting, audiences |
| ads-serving | RTB auction, ad selection (< 100ms) |
| ads-billing | Prepaid/postpaid, invoicing |
| ads-tracking | Pixel events, attribution |
| ads-analytics | Metrics, reporting, insights |
Redis Key Patterns
| Key | Type | TTL | Purpose |
|---|---|---|---|
ads:active:{placement} |
Sorted Set | 5 min | Active ads by eCPM |
freq:{userId}:{date} |
Hash | 24h | User ad frequency |
budget:{campaignId}:{date} |
String | 24h | Daily spend |
target:{criteria} |
Set | 1h | Eligible ad sets |
Metrics
| Metric | Formula |
|---|---|
| CTR | Clicks / Impressions |
| CPC | Spend / Clicks |
| CPM | (Spend / Impressions) × 1000 |
| CPA | Spend / Conversions |
| ROAS | Revenue / Spend |