diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Events/AdServingEventPublisher.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Events/AdServingEventPublisher.cs
new file mode 100644
index 00000000..8aa8dca4
--- /dev/null
+++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Events/AdServingEventPublisher.cs
@@ -0,0 +1,102 @@
+namespace AdsServingService.API.Application.Events;
+
+///
+/// EN: Event payload for served-ad event.
+/// VI: Payload event cho sự kiện quảng cáo đã được phục vụ.
+///
+public record AdServedEvent
+{
+ public Guid AuctionId { get; init; }
+ public Guid AdId { get; init; }
+ public Guid CampaignId { get; init; }
+ public Guid UserId { get; init; }
+ public string PlacementType { get; init; } = string.Empty;
+ public decimal FinalPrice { get; init; }
+ public decimal WinningEcpm { get; init; }
+ public DateTime ServedAt { get; init; }
+}
+
+///
+/// EN: Event payload for impression.
+/// VI: Payload event cho impression.
+///
+public record AdImpressionTrackedEvent
+{
+ public Guid AdId { get; init; }
+ public Guid UserId { get; init; }
+ public DateTime Timestamp { get; init; }
+}
+
+///
+/// EN: Event payload for click.
+/// VI: Payload event cho click.
+///
+public record AdClickTrackedEvent
+{
+ public Guid AdId { get; init; }
+ public Guid UserId { get; init; }
+ public DateTime Timestamp { get; init; }
+}
+
+///
+/// EN: Abstraction for publishing ads-serving events asynchronously.
+/// VI: Abstraction để publish bất đồng bộ các sự kiện ads-serving.
+///
+public interface IAdServingEventPublisher
+{
+ Task PublishAdServedAsync(AdServedEvent payload, CancellationToken cancellationToken);
+ Task PublishImpressionTrackedAsync(AdImpressionTrackedEvent payload, CancellationToken cancellationToken);
+ Task PublishClickTrackedAsync(AdClickTrackedEvent payload, CancellationToken cancellationToken);
+}
+
+///
+/// EN: Logging-based publisher used as default implementation.
+/// VI: Publisher dựa trên logging dùng làm implementation mặc định.
+///
+public class LoggingAdServingEventPublisher : IAdServingEventPublisher
+{
+ private readonly ILogger _logger;
+
+ public LoggingAdServingEventPublisher(ILogger logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public Task PublishAdServedAsync(AdServedEvent payload, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation(
+ "ad.served.v1 event published: AuctionId={AuctionId}, AdId={AdId}, CampaignId={CampaignId}, UserId={UserId}, PlacementType={PlacementType}, FinalPrice={FinalPrice}, WinningEcpm={WinningEcpm}, ServedAt={ServedAt}",
+ payload.AuctionId,
+ payload.AdId,
+ payload.CampaignId,
+ payload.UserId,
+ payload.PlacementType,
+ payload.FinalPrice,
+ payload.WinningEcpm,
+ payload.ServedAt);
+
+ return Task.CompletedTask;
+ }
+
+ public Task PublishImpressionTrackedAsync(AdImpressionTrackedEvent payload, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation(
+ "ads.impression.tracked.v1 event published: AdId={AdId}, UserId={UserId}, Timestamp={Timestamp}",
+ payload.AdId,
+ payload.UserId,
+ payload.Timestamp);
+
+ return Task.CompletedTask;
+ }
+
+ public Task PublishClickTrackedAsync(AdClickTrackedEvent payload, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation(
+ "ads.click.tracked.v1 event published: AdId={AdId}, UserId={UserId}, Timestamp={Timestamp}",
+ payload.AdId,
+ payload.UserId,
+ payload.Timestamp);
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionStatisticsQuery.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionStatisticsQuery.cs
new file mode 100644
index 00000000..0450142a
--- /dev/null
+++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionStatisticsQuery.cs
@@ -0,0 +1,21 @@
+using MediatR;
+
+namespace AdsServingService.API.Application.Queries;
+
+///
+/// EN: Query to retrieve aggregate auction statistics.
+/// VI: Query để lấy thống kê tổng hợp của auction.
+///
+public record GetAuctionStatisticsQuery : IRequest;
+
+///
+/// EN: Aggregate auction statistics DTO.
+/// VI: DTO thống kê tổng hợp auction.
+///
+public record AuctionStatisticsDto
+{
+ public int TotalAuctions { get; init; }
+ public decimal AverageWinRate { get; init; }
+ public decimal AverageeCPM { get; init; }
+ public long TotalBidsPlaced { get; init; }
+}
diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionStatisticsQueryHandler.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionStatisticsQueryHandler.cs
new file mode 100644
index 00000000..f364a800
--- /dev/null
+++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionStatisticsQueryHandler.cs
@@ -0,0 +1,82 @@
+using System.Text.Json;
+using AdsServingService.Infrastructure;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace AdsServingService.API.Application.Queries;
+
+///
+/// EN: Handler for GetAuctionStatisticsQuery.
+/// VI: Handler cho GetAuctionStatisticsQuery.
+///
+public class GetAuctionStatisticsQueryHandler
+ : IRequestHandler
+{
+ private readonly AdsServingServiceContext _context;
+
+ public GetAuctionStatisticsQueryHandler(AdsServingServiceContext context)
+ {
+ _context = context ?? throw new ArgumentNullException(nameof(context));
+ }
+
+ public async Task Handle(
+ GetAuctionStatisticsQuery request,
+ CancellationToken cancellationToken)
+ {
+ var auctionSnapshots = await _context.Auctions
+ .AsNoTracking()
+ .Select(auction => new
+ {
+ HasWinner = auction.Result != null,
+ WinningEcpm = auction.Result != null ? auction.Result.WinningeCPM : 0m,
+ BidsJson = EF.Property(auction, "_bidsJson"),
+ })
+ .ToListAsync(cancellationToken);
+
+ var totalAuctions = auctionSnapshots.Count;
+ var totalWinners = auctionSnapshots.Count(snapshot => snapshot.HasWinner);
+ var averageWinRate = totalAuctions == 0
+ ? 0m
+ : Math.Round((decimal)totalWinners / totalAuctions * 100m, 2);
+
+ var winningEcpmValues = auctionSnapshots
+ .Where(snapshot => snapshot.HasWinner)
+ .Select(snapshot => snapshot.WinningEcpm)
+ .ToList();
+
+ var averageEcpm = winningEcpmValues.Count == 0
+ ? 0m
+ : Math.Round(winningEcpmValues.Average(), 4);
+
+ var totalBidsPlaced = auctionSnapshots
+ .Sum(snapshot => CountBids(snapshot.BidsJson));
+
+ return new AuctionStatisticsDto
+ {
+ TotalAuctions = totalAuctions,
+ AverageWinRate = averageWinRate,
+ AverageeCPM = averageEcpm,
+ TotalBidsPlaced = totalBidsPlaced,
+ };
+ }
+
+ private static int CountBids(string? bidsJson)
+ {
+ if (string.IsNullOrWhiteSpace(bidsJson))
+ {
+ return 0;
+ }
+
+ try
+ {
+ using var document = JsonDocument.Parse(bidsJson);
+ return document.RootElement.ValueKind == JsonValueKind.Array
+ ? document.RootElement.GetArrayLength()
+ : 0;
+ }
+ catch (JsonException)
+ {
+ return 0;
+ }
+ }
+}
diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionsQueryHandler.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionsQueryHandler.cs
index 1602e908..92308f7c 100644
--- a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionsQueryHandler.cs
+++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/GetAuctionsQueryHandler.cs
@@ -1,6 +1,7 @@
using AdsServingService.Infrastructure;
using MediatR;
using Microsoft.EntityFrameworkCore;
+using System.Text.Json;
namespace AdsServingService.API.Application.Queries;
@@ -42,25 +43,59 @@ public class GetAuctionsQueryHandler : IRequestHandler EF.Property(a, "_auctionTime"))
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
- .Select(a => new AuctionDto
+ .Select(a => new
{
Id = a.Id,
UserId = a.UserId,
PlacementType = a.PlacementType,
AuctionTime = EF.Property(a, "_auctionTime"),
- BidCount = a.Bids.Count,
+ BidsJson = EF.Property(a, "_bidsJson"),
WinningAdId = a.Result != null ? a.Result.WinningAdId : null,
FinalPrice = a.Result != null ? a.Result.FinalPrice : null,
WinningeCPM = a.Result != null ? a.Result.WinningeCPM : null
})
.ToListAsync(cancellationToken);
+ var items = auctions
+ .Select(a => new AuctionDto
+ {
+ Id = a.Id,
+ UserId = a.UserId,
+ PlacementType = a.PlacementType,
+ AuctionTime = a.AuctionTime,
+ BidCount = CountBids(a.BidsJson),
+ WinningAdId = a.WinningAdId,
+ FinalPrice = a.FinalPrice,
+ WinningeCPM = a.WinningeCPM,
+ })
+ .ToList();
+
return new PagedResult
{
- Items = auctions,
+ Items = items,
TotalCount = totalCount,
Page = request.Page,
PageSize = request.PageSize
};
}
+
+ private static int CountBids(string? bidsJson)
+ {
+ if (string.IsNullOrWhiteSpace(bidsJson))
+ {
+ return 0;
+ }
+
+ try
+ {
+ using var document = JsonDocument.Parse(bidsJson);
+ return document.RootElement.ValueKind == JsonValueKind.Array
+ ? document.RootElement.GetArrayLength()
+ : 0;
+ }
+ catch (JsonException)
+ {
+ return 0;
+ }
+ }
}
diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/ServeAdQueryHandler.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/ServeAdQueryHandler.cs
index 04e556d3..dfd71ee1 100644
--- a/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/ServeAdQueryHandler.cs
+++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Queries/ServeAdQueryHandler.cs
@@ -1,4 +1,8 @@
using AdsServingService.Domain.AggregatesModel.AuctionAggregate;
+using AdsServingService.API.Application.Events;
+using AdsServingService.API.Application.Services;
+using AdsServingService.Infrastructure;
+using System.Text.Json;
using MediatR;
namespace AdsServingService.API.Application.Queries;
@@ -9,10 +13,23 @@ namespace AdsServingService.API.Application.Queries;
///
public class ServeAdQueryHandler : IRequestHandler
{
+ private readonly AdsServingServiceContext _context;
+ private readonly IEligibleAdsProvider _eligibleAdsProvider;
+ private readonly IAuctionScoringService _auctionScoringService;
+ private readonly IAdServingEventPublisher _eventPublisher;
private readonly ILogger _logger;
- public ServeAdQueryHandler(ILogger logger)
+ public ServeAdQueryHandler(
+ AdsServingServiceContext context,
+ IEligibleAdsProvider eligibleAdsProvider,
+ IAuctionScoringService auctionScoringService,
+ IAdServingEventPublisher eventPublisher,
+ ILogger logger)
{
+ _context = context ?? throw new ArgumentNullException(nameof(context));
+ _eligibleAdsProvider = eligibleAdsProvider ?? throw new ArgumentNullException(nameof(eligibleAdsProvider));
+ _auctionScoringService = auctionScoringService ?? throw new ArgumentNullException(nameof(auctionScoringService));
+ _eventPublisher = eventPublisher ?? throw new ArgumentNullException(nameof(eventPublisher));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -22,9 +39,11 @@ public class ServeAdQueryHandler : IRequestHandler
try
{
- // TODO: Step 1 - Fetch eligible ads from cache/database
- // For now, mock data for demonstration
- var eligibleAds = GetMockEligibleAds(request.PlacementType);
+ var eligibleAds = await _eligibleAdsProvider.GetEligibleAdsAsync(
+ request.UserId,
+ request.PlacementType,
+ request.UserContext,
+ cancellationToken);
if (!eligibleAds.Any())
{
@@ -38,12 +57,17 @@ public class ServeAdQueryHandler : IRequestHandler
// Step 3 - Add bids for each eligible ad
foreach (var ad in eligibleAds)
{
- // TODO: Get actual bid amount, CTR prediction, quality score
- var bidAmount = ad.BidAmount;
- var predictedCTR = 0.02m; // 2% CTR (mock)
- var qualityScore = 1.0m; // Perfect quality (mock)
+ var signals = _auctionScoringService.Score(
+ ad,
+ request.PlacementType,
+ request.UserContext);
- auction.AddBid(ad.AdId, ad.CampaignId, bidAmount, predictedCTR, qualityScore);
+ auction.AddBid(
+ ad.AdId,
+ ad.CampaignId,
+ ad.BidAmount,
+ signals.PredictedCtr,
+ signals.QualityScore);
}
// Step 4 - Run auction
@@ -58,8 +82,33 @@ public class ServeAdQueryHandler : IRequestHandler
// Step 5 - Get winning ad details
var winningAd = eligibleAds.First(a => a.AdId == auction.Result.WinningAdId);
- // TODO: Step 6 - Fire async events (impression tracking, billing)
-
+ _context.Auctions.Add(auction);
+ _context.Entry(auction).Property("_bidsJson").CurrentValue = JsonSerializer.Serialize(
+ auction.Bids.Select(bid => new
+ {
+ bid.AdId,
+ bid.CampaignId,
+ bid.BidAmount,
+ bid.PredictedCTR,
+ bid.QualityScore,
+ bid.eCPM,
+ }));
+ await _context.SaveEntitiesAsync(cancellationToken);
+
+ await _eventPublisher.PublishAdServedAsync(
+ new AdServedEvent
+ {
+ AuctionId = auction.Id,
+ AdId = winningAd.AdId,
+ CampaignId = winningAd.CampaignId,
+ UserId = request.UserId,
+ PlacementType = request.PlacementType,
+ FinalPrice = auction.Result.FinalPrice,
+ WinningEcpm = auction.Result.WinningeCPM,
+ ServedAt = DateTime.UtcNow,
+ },
+ cancellationToken);
+
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
_logger.LogInformation(
"Ad served in {Elapsed}ms - AdId: {AdId}, eCPM: {eCPM}, Price: {Price}",
@@ -85,49 +134,4 @@ public class ServeAdQueryHandler : IRequestHandler
return null;
}
}
-
- // Mock data for demonstration
- private List GetMockEligibleAds(string placementType)
- {
- return new List
- {
- new MockAdData
- {
- AdId = Guid.NewGuid(),
- CampaignId = Guid.NewGuid(),
- BidAmount = 5000, // 5000 VND CPC
- Format = "single_image",
- Headline = "Amazing Product - Limited Offer!",
- PrimaryText = "Get 50% off today only",
- CallToAction = "Shop Now",
- CreativeUrl = "https://example.com/creative.jpg",
- DestinationUrl = "https://example.com/product"
- },
- new MockAdData
- {
- AdId = Guid.NewGuid(),
- CampaignId = Guid.NewGuid(),
- BidAmount = 4500,
- Format = "single_image",
- Headline = "Summer Sale",
- PrimaryText = "Up to 70% discount",
- CallToAction = "Learn More",
- CreativeUrl = "https://example.com/creative2.jpg",
- DestinationUrl = "https://example.com/sale"
- }
- };
- }
-}
-
-internal record MockAdData
-{
- public Guid AdId { get; init; }
- public Guid CampaignId { get; init; }
- public decimal BidAmount { get; init; }
- public string Format { get; init; } = null!;
- public string? Headline { get; init; }
- public string? PrimaryText { get; init; }
- public string? CallToAction { get; init; }
- public string? CreativeUrl { get; init; }
- public string? DestinationUrl { get; init; }
}
diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Services/AuctionScoringService.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Services/AuctionScoringService.cs
new file mode 100644
index 00000000..35ab07a2
--- /dev/null
+++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Services/AuctionScoringService.cs
@@ -0,0 +1,65 @@
+namespace AdsServingService.API.Application.Services;
+
+///
+/// EN: Scoring signal tuple used to place bids in auction.
+/// VI: Bộ tín hiệu scoring dùng để đặt bid trong auction.
+///
+public record AuctionBidSignals(decimal PredictedCtr, decimal QualityScore);
+
+///
+/// EN: Abstraction for scoring hooks in ad serving auction.
+/// VI: Abstraction cho scoring hook trong auction phục vụ quảng cáo.
+///
+public interface IAuctionScoringService
+{
+ AuctionBidSignals Score(
+ EligibleAdCandidate candidate,
+ string placementType,
+ IReadOnlyDictionary userContext);
+}
+
+///
+/// EN: Default heuristic scoring strategy.
+/// VI: Chiến lược scoring heuristic mặc định.
+///
+public class DefaultAuctionScoringService : IAuctionScoringService
+{
+ public AuctionBidSignals Score(
+ EligibleAdCandidate candidate,
+ string placementType,
+ IReadOnlyDictionary userContext)
+ {
+ var baseCtr = placementType.ToLowerInvariant() switch
+ {
+ "story" => 0.018m,
+ "banner" => 0.012m,
+ _ => 0.020m,
+ };
+
+ var segmentBoost = 1.0m;
+ if (userContext.TryGetValue("segment", out var segmentValue) &&
+ segmentValue.Equals("high-intent", StringComparison.OrdinalIgnoreCase))
+ {
+ segmentBoost += 0.25m;
+ }
+
+ var recencyBoost = 1.0m;
+ if (userContext.TryGetValue("recentPurchaseDays", out var recencyRaw) &&
+ int.TryParse(recencyRaw, out var recencyDays) &&
+ recencyDays <= 14)
+ {
+ recencyBoost += 0.10m;
+ }
+
+ var predictedCtr = Math.Round(baseCtr * segmentBoost * recencyBoost, 4);
+
+ var qualityScore = candidate.Format.ToLowerInvariant() switch
+ {
+ "single_image" => 1.0m,
+ "video" => 1.1m,
+ _ => 0.95m,
+ };
+
+ return new AuctionBidSignals(predictedCtr, qualityScore);
+ }
+}
diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Application/Services/EligibleAdsProvider.cs b/services/ads-serving-service-net/src/AdsServingService.API/Application/Services/EligibleAdsProvider.cs
new file mode 100644
index 00000000..410ace7c
--- /dev/null
+++ b/services/ads-serving-service-net/src/AdsServingService.API/Application/Services/EligibleAdsProvider.cs
@@ -0,0 +1,89 @@
+namespace AdsServingService.API.Application.Services;
+
+///
+/// EN: Candidate ad model used in serving pipeline.
+/// VI: Model quảng cáo ứng viên dùng trong pipeline phục vụ quảng cáo.
+///
+public record EligibleAdCandidate
+{
+ public Guid AdId { get; init; }
+ public Guid CampaignId { get; init; }
+ public decimal BidAmount { get; init; }
+ public string Format { get; init; } = null!;
+ public string? Headline { get; init; }
+ public string? PrimaryText { get; init; }
+ public string? CallToAction { get; init; }
+ public string? CreativeUrl { get; init; }
+ public string? DestinationUrl { get; init; }
+}
+
+///
+/// EN: Service abstraction to load eligible ads for a serve request.
+/// VI: Abstraction service để tải danh sách quảng cáo đủ điều kiện cho request serve.
+///
+public interface IEligibleAdsProvider
+{
+ Task> GetEligibleAdsAsync(
+ Guid userId,
+ string placementType,
+ IReadOnlyDictionary userContext,
+ CancellationToken cancellationToken);
+}
+
+///
+/// EN: In-memory eligible ads provider used as default fallback.
+/// VI: Provider quảng cáo đủ điều kiện dạng in-memory dùng như fallback mặc định.
+///
+public class InMemoryEligibleAdsProvider : IEligibleAdsProvider
+{
+ public Task> GetEligibleAdsAsync(
+ Guid userId,
+ string placementType,
+ IReadOnlyDictionary userContext,
+ CancellationToken cancellationToken)
+ {
+ // EN: Keep deterministic IDs per request/user so placement is reproducible.
+ // VI: Giữ ID xác định theo request/user để kết quả placement có thể lặp lại.
+ var hashSeed = Math.Abs(HashCode.Combine(userId, placementType.ToLowerInvariant()));
+ var primaryCampaignId = DeterministicGuid($"{placementType}:{hashSeed}:campaign:primary");
+ var secondaryCampaignId = DeterministicGuid($"{placementType}:{hashSeed}:campaign:secondary");
+
+ IReadOnlyList candidates =
+ [
+ new EligibleAdCandidate
+ {
+ AdId = DeterministicGuid($"{placementType}:{hashSeed}:ad:primary"),
+ CampaignId = primaryCampaignId,
+ BidAmount = 5000m,
+ Format = "single_image",
+ Headline = "Amazing Product - Limited Offer!",
+ PrimaryText = "Get 50% off today only",
+ CallToAction = "Shop Now",
+ CreativeUrl = "https://example.com/creative.jpg",
+ DestinationUrl = "https://example.com/product",
+ },
+ new EligibleAdCandidate
+ {
+ AdId = DeterministicGuid($"{placementType}:{hashSeed}:ad:secondary"),
+ CampaignId = secondaryCampaignId,
+ BidAmount = 4500m,
+ Format = "single_image",
+ Headline = "Summer Sale",
+ PrimaryText = "Up to 70% discount",
+ CallToAction = "Learn More",
+ CreativeUrl = "https://example.com/creative2.jpg",
+ DestinationUrl = "https://example.com/sale",
+ },
+ ];
+
+ return Task.FromResult(candidates);
+ }
+
+ private static Guid DeterministicGuid(string input)
+ {
+ var bytes = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input));
+ var guidBytes = new byte[16];
+ Array.Copy(bytes, guidBytes, guidBytes.Length);
+ return new Guid(guidBytes);
+ }
+}
diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdminAuctionsController.cs b/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdminAuctionsController.cs
index 2bf41c21..acd89c58 100644
--- a/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdminAuctionsController.cs
+++ b/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdminAuctionsController.cs
@@ -56,25 +56,10 @@ public class AdminAuctionsController : ControllerBase
///
[HttpGet("statistics")]
[ProducesResponseType(typeof(AuctionStatisticsDto), StatusCodes.Status200OK)]
- public ActionResult GetStatistics()
+ public async Task> GetStatistics()
{
- // TODO: Implement statistics query handler
_logger.LogInformation("Auction statistics requested");
-
- return Ok(new AuctionStatisticsDto
- {
- TotalAuctions = 0,
- AverageWinRate = 0,
- AverageeCPM = 0,
- TotalBidsPlaced = 0
- });
+ var statistics = await _mediator.Send(new GetAuctionStatisticsQuery());
+ return Ok(statistics);
}
}
-
-public record AuctionStatisticsDto
-{
- public int TotalAuctions { get; init; }
- public decimal AverageWinRate { get; init; }
- public decimal AverageeCPM { get; init; }
- public long TotalBidsPlaced { get; init; }
-}
diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdsController.cs b/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdsController.cs
index 2fb2bc31..ce7e2777 100644
--- a/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdsController.cs
+++ b/services/ads-serving-service-net/src/AdsServingService.API/Controllers/AdsController.cs
@@ -1,4 +1,5 @@
using AdsServingService.API.Application.Queries;
+using AdsServingService.API.Application.Events;
using MediatR;
using Microsoft.AspNetCore.Mvc;
@@ -14,11 +15,16 @@ namespace AdsServingService.API.Controllers;
public class AdsController : ControllerBase
{
private readonly IMediator _mediator;
+ private readonly IAdServingEventPublisher _eventPublisher;
private readonly ILogger _logger;
- public AdsController(IMediator mediator, ILogger logger)
+ public AdsController(
+ IMediator mediator,
+ IAdServingEventPublisher eventPublisher,
+ ILogger logger)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
+ _eventPublisher = eventPublisher ?? throw new ArgumentNullException(nameof(eventPublisher));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -52,14 +58,18 @@ public class AdsController : ControllerBase
///
[HttpPost("events/impression")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
- public IActionResult TrackImpression([FromBody] ImpressionEvent impressionEvent)
+ public async Task TrackImpression([FromBody] ImpressionEvent impressionEvent)
{
_logger.LogInformation("Impression tracked for Ad {AdId}", impressionEvent.AdId);
-
- // TODO: Publish to RabbitMQ for async processing
- // - ads-billing (charge advertiser)
- // - ads-tracking (attribution)
- // - ads-analytics (metrics)
+
+ await _eventPublisher.PublishImpressionTrackedAsync(
+ new AdImpressionTrackedEvent
+ {
+ AdId = impressionEvent.AdId,
+ UserId = impressionEvent.UserId,
+ Timestamp = impressionEvent.Timestamp,
+ },
+ HttpContext.RequestAborted);
return Accepted();
}
@@ -70,11 +80,18 @@ public class AdsController : ControllerBase
///
[HttpPost("events/click")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
- public IActionResult TrackClick([FromBody] ClickEvent clickEvent)
+ public async Task TrackClick([FromBody] ClickEvent clickEvent)
{
_logger.LogInformation("Click tracked for Ad {AdId}", clickEvent.AdId);
-
- // TODO: Publish to RabbitMQ for async processing
+
+ await _eventPublisher.PublishClickTrackedAsync(
+ new AdClickTrackedEvent
+ {
+ AdId = clickEvent.AdId,
+ UserId = clickEvent.UserId,
+ Timestamp = clickEvent.Timestamp,
+ },
+ HttpContext.RequestAborted);
return Accepted();
}
diff --git a/services/ads-serving-service-net/src/AdsServingService.API/Program.cs b/services/ads-serving-service-net/src/AdsServingService.API/Program.cs
index e6e5ed9d..f788dadb 100644
--- a/services/ads-serving-service-net/src/AdsServingService.API/Program.cs
+++ b/services/ads-serving-service-net/src/AdsServingService.API/Program.cs
@@ -1,4 +1,6 @@
using Asp.Versioning;
+using AdsServingService.API.Application.Events;
+using AdsServingService.API.Application.Services;
using FluentValidation;
using Hellang.Middleware.ProblemDetails;
using AdsServingService.API.Application.Behaviors;
@@ -25,6 +27,9 @@ try
// EN: Add Infrastructure services / VI: Thêm Infrastructure services
builder.Services.AddInfrastructure(builder.Configuration);
+ builder.Services.AddScoped();
+ builder.Services.AddScoped();
+ builder.Services.AddScoped();
// EN: Add MediatR with behaviors / VI: Thêm MediatR với behaviors
builder.Services.AddMediatR(cfg =>