Files
pos-system/microservices/.agent/skills/ads-development/SKILL.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

15 KiB
Raw Blame History

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
author version
Velik Ho 1.0

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

Resources / Tài Nguyên