feat: add MagicWalletCycleSeedService — one-time BackgroundService to seed ClubMembershipCycle for existing active memberships, controlled via appsettings SeedWorkers:MagicWalletCycleSeed:Enabled
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m36s

This commit is contained in:
masoodafar-web
2026-02-22 21:14:09 +03:30
parent df976b7b80
commit 305b8d3fd8
4 changed files with 140 additions and 0 deletions
@@ -0,0 +1,126 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Club;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Infrastructure.BackgroundServices;
/// <summary>
/// سرویس یکباره برای Seed کردن ClubMembershipCycle برای عضویت‌های فعال موجود.
/// عضویت‌هایی که قبل از اضافه شدن سیستم Cycle ایجاد شده‌اند رکورد ندارند.
/// این Worker در استارتاپ اجرا شده، برای آنها CycleNumber=1 می‌سازد و سپس متوقف می‌شود.
///
/// فعال/غیرفعال از appsettings:
/// "SeedWorkers": { "MagicWalletCycleSeed": { "Enabled": true } }
///
/// بعد از اجرای موفق، Enabled را false کنید.
/// </summary>
public class MagicWalletCycleSeedService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<MagicWalletCycleSeedService> _logger;
public MagicWalletCycleSeedService(
IServiceScopeFactory scopeFactory,
ILogger<MagicWalletCycleSeedService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// صبر کوتاه برای اطمینان از آماده شدن دیتابیس
await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken);
_logger.LogInformation("MagicWalletCycleSeedService started — scanning active memberships without cycles...");
try
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>();
var seededCount = await SeedMissingCycles(context, stoppingToken);
_logger.LogInformation(
"MagicWalletCycleSeedService completed — seeded {Count} ClubMembershipCycle records",
seededCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "MagicWalletCycleSeedService encountered an error");
}
_logger.LogInformation(
"MagicWalletCycleSeedService finished — shutting down (one-time execution). " +
"Set SeedWorkers:MagicWalletCycleSeed:Enabled to false in appsettings.");
}
private async Task<int> SeedMissingCycles(IApplicationDbContext context, CancellationToken ct)
{
// پیدا کردن عضویت‌های فعالی که هنوز Cycle ندارند
var membershipIdsWithCycle = await context.ClubMembershipCycles
.Where(c => !c.IsDeleted)
.Select(c => c.ClubMembershipId)
.Distinct()
.ToListAsync(ct);
var membershipsWithoutCycle = await context.ClubMemberships
.Where(cm => !cm.IsDeleted && cm.IsActive && !membershipIdsWithCycle.Contains(cm.Id))
.Select(cm => new
{
cm.Id,
cm.UserId,
cm.ActivatedAt
})
.ToListAsync(ct);
if (membershipsWithoutCycle.Count == 0)
{
_logger.LogInformation("No active memberships without cycles found — nothing to seed");
return 0;
}
_logger.LogInformation("Found {Count} active memberships without ClubMembershipCycle — seeding...",
membershipsWithoutCycle.Count);
var seeded = 0;
foreach (var cm in membershipsWithoutCycle)
{
if (ct.IsCancellationRequested) break;
try
{
var cycle = new ClubMembershipCycle
{
UserId = cm.UserId,
ClubMembershipId = cm.Id,
CycleNumber = 1,
PackagePurchasedAt = cm.ActivatedAt ?? DateTime.UtcNow,
IsCurrentCycle = true,
PurchaseMethod = 0,
PackageAmount = 0
};
context.ClubMembershipCycles.Add(cycle);
seeded++;
_logger.LogDebug(
"Seeded ClubMembershipCycle for UserId={UserId}, MembershipId={MembershipId}, ActivatedAt={ActivatedAt}",
cm.UserId, cm.Id, cm.ActivatedAt);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Failed to seed cycle for MembershipId={MembershipId}, UserId={UserId}",
cm.Id, cm.UserId);
}
}
await context.SaveChangesAsync(ct);
return seeded;
}
}
@@ -126,6 +126,10 @@ public static class ConfigureServices
// One-time: Initialize inventory records for existing products
services.AddHostedService<InventoryInitializerService>();
// One-time: Seed ClubMembershipCycle for existing active memberships
if (configuration.GetValue<bool>("SeedWorkers:MagicWalletCycleSeed:Enabled"))
services.AddHostedService<MagicWalletCycleSeedService>();
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
{
services.AddDbContext<ApplicationDbContext>(options =>
@@ -68,6 +68,11 @@
"CronExpression": "5 0 * * 0"
}
},
"SeedWorkers": {
"MagicWalletCycleSeed": {
"Enabled": true
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
@@ -75,6 +75,11 @@
"CronExpression": "5 0 * * 0"
}
},
"SeedWorkers": {
"MagicWalletCycleSeed": {
"Enabled": true
}
},
"AllowedHosts": "*",
"Authentication": {
"Authority": "https://ids.domain.com/",