feat: implement Chatika account activation job for new club members

This commit is contained in:
masoodafar-web
2025-12-23 22:52:49 +03:30
parent 30144a07c8
commit 288b85a59b
10 changed files with 908 additions and 2 deletions
@@ -261,8 +261,9 @@ public class AcceptClubMembershipContractCommandHandler
// 10. اعطای ویژگی‌های باشگاه برای کاربر (فقط برای عضویت جدید)
if (isNewMembership)
{
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
var clubFeatures = await _context.ClubFeatures
.Where(f => !f.IsDeleted && new long[] { 1, 2, 3, 4 }.Contains(f.Id))
.Where(f => !f.IsDeleted && featureIds.Contains(f.Id))
.ToListAsync(cancellationToken);
if (clubFeatures.Any())
@@ -297,8 +297,9 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
// 9. اضافه کردن ویژگی‌های باشگاه برای کاربر (فقط برای عضویت جدید)
if (isNewMembership)
{
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
var clubFeatures = await _context.ClubFeatures
.Where(f => !f.IsDeleted && new long[] { 1, 2, 3, 4 }.Contains(f.Id))
.Where(f => !f.IsDeleted && featureIds.Contains(f.Id))
.ToListAsync(cancellationToken);
if (clubFeatures.Any())
@@ -0,0 +1,64 @@
namespace CMSMicroservice.Application.Common.Interfaces;
/// <summary>
/// سرویس ارتباط با API چتیکا برای ساخت حساب کاربری
/// </summary>
public interface IChatikaApiService
{
/// <summary>
/// ایجاد حساب کاربری در چتیکا
/// </summary>
/// <param name="mobileNumber">شماره موبایل کاربر</param>
/// <param name="fullName">نام کامل کاربر</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>نتیجه ایجاد حساب</returns>
Task<ChatikaAccountResult> CreateAccountAsync(
string mobileNumber,
string fullName,
CancellationToken cancellationToken = default);
}
/// <summary>
/// نتیجه ایجاد حساب در چتیکا
/// </summary>
public class ChatikaAccountResult
{
/// <summary>
/// موفقیت عملیات
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// پیام خطا در صورت عدم موفقیت
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// شناسه کاربر در چتیکا
/// </summary>
public string? ChatikaUserId { get; set; }
/// <summary>
/// URL دسترسی به چتیکا
/// </summary>
public string? AccessUrl { get; set; }
/// <summary>
/// ایجاد نتیجه موفق
/// </summary>
public static ChatikaAccountResult Success(string? chatikaUserId = null, string? accessUrl = null) => new()
{
IsSuccess = true,
ChatikaUserId = chatikaUserId,
AccessUrl = accessUrl
};
/// <summary>
/// ایجاد نتیجه ناموفق
/// </summary>
public static ChatikaAccountResult Failure(string errorMessage) => new()
{
IsSuccess = false,
ErrorMessage = errorMessage
};
}
@@ -0,0 +1,58 @@
namespace CMSMicroservice.Domain.Enums;
/// <summary>
/// انواع ویژگی‌های باشگاه مشتریان
/// </summary>
public enum ClubFeatureType
{
/// <summary>
/// چتیکا - دستیار هوش مصنوعی
/// </summary>
Chatika = 1,
/// <summary>
/// بیمه - خدمات بیمه‌ای
/// </summary>
Bime = 2,
/// <summary>
/// تریپ - خدمات سفر و گردشگری
/// </summary>
Trip = 3,
/// <summary>
/// لرن - آموزش و یادگیری
/// </summary>
Learn = 4
}
/// <summary>
/// Extension methods برای ClubFeatureType
/// </summary>
public static class ClubFeatureTypeExtensions
{
/// <summary>
/// دریافت تمام مقادیر ClubFeatureType به صورت آرایه long
/// </summary>
public static long[] GetAllFeatureIds()
{
return Enum.GetValues<ClubFeatureType>()
.Select(f => (long)f)
.ToArray();
}
/// <summary>
/// دریافت عنوان فارسی ویژگی
/// </summary>
public static string GetPersianTitle(this ClubFeatureType featureType)
{
return featureType switch
{
ClubFeatureType.Chatika => "چتیکا",
ClubFeatureType.Bime => "بیمه",
ClubFeatureType.Trip => "تور و سفر",
ClubFeatureType.Learn => "آموزش",
_ => featureType.ToString()
};
}
}
@@ -0,0 +1,183 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Club;
using CMSMicroservice.Domain.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Polly;
namespace CMSMicroservice.Infrastructure.BackgroundJobs;
/// <summary>
/// Hangfire Job برای فعال‌سازی حساب چتیکا برای اعضای جدید باشگاه
/// این Job کاربرانی که باشگاهشان فعال شده ولی حساب چتیکا ندارند را پیدا کرده و حساب می‌سازد
/// </summary>
public class ChatikaAccountActivationJob
{
private readonly IApplicationDbContext _context;
private readonly IChatikaApiService _chatikaApiService;
private readonly ILogger<ChatikaAccountActivationJob> _logger;
private readonly ResiliencePipeline _retryPipeline;
/// <summary>
/// توضیحات فارسی فیچر چتیکا
/// </summary>
private const string ChatikaFeatureDescription =
"🎉 تبریک! حساب هوش مصنوعی چتیکا شما فعال شد.\n\n" +
"برای استفاده از امکانات رایگان چتیکا:\n" +
"1️⃣ به وب‌سایت chatika.ir مراجعه کنید\n" +
"2️⃣ شماره موبایل خود را وارد کنید\n" +
"3️⃣ از دستیار هوشمند چتیکا لذت ببرید!\n\n" +
"🔗 لینک ورود: https://chatika.ir";
public ChatikaAccountActivationJob(
IApplicationDbContext context,
IChatikaApiService chatikaApiService,
ILogger<ChatikaAccountActivationJob> logger)
{
_context = context;
_chatikaApiService = chatikaApiService;
_logger = logger;
// Polly Retry: 3 تلاش با فاصله نمایی
_retryPipeline = new ResiliencePipelineBuilder()
.AddRetry(new Polly.Retry.RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(30),
BackoffType = Polly.DelayBackoffType.Exponential,
UseJitter = true,
OnRetry = args =>
{
_logger.LogWarning(
"⚠️ Retry attempt {AttemptNumber} for Chatika API. Exception: {ExceptionType}",
args.AttemptNumber,
args.Outcome.Exception?.GetType().Name ?? "None");
return ValueTask.CompletedTask;
}
})
.Build();
}
/// <summary>
/// اجرای Job برای فعال‌سازی حساب چتیکا
/// </summary>
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
{
var executionId = Guid.NewGuid();
_logger.LogInformation(
"🚀 [{ExecutionId}] Starting Chatika account activation job",
executionId);
try
{
// پیدا کردن کاربرانی که:
// 1. باشگاه فعال دارند (ClubMembership.IsActive = true)
// 2. فیچر چتیکا (Id=1) رو دارند
// 3. فیچر چتیکاشون هنوز Notes نداره (یعنی حساب ساخته نشده)
var pendingUsers = await _context.UserClubFeatures
.Include(ucf => ucf.User)
.Include(ucf => ucf.ClubMembership)
.Where(ucf =>
ucf.ClubFeatureId == (long)ClubFeatureType.Chatika &&
ucf.ClubMembership.IsActive &&
!ucf.IsDeleted &&
ucf.IsActive &&
(ucf.Notes == null || ucf.Notes == "")) // حساب هنوز ساخته نشده
.ToListAsync(cancellationToken);
if (!pendingUsers.Any())
{
_logger.LogInformation(
"✅ [{ExecutionId}] No pending users for Chatika activation",
executionId);
return;
}
_logger.LogInformation(
"📋 [{ExecutionId}] Found {Count} users pending Chatika activation",
executionId,
pendingUsers.Count);
var successCount = 0;
var failCount = 0;
foreach (var userFeature in pendingUsers)
{
try
{
// بررسی تکراری نبودن (Double-check)
if (!string.IsNullOrEmpty(userFeature.Notes))
{
_logger.LogDebug(
"⏭️ Skipping user {UserId} - already processed",
userFeature.UserId);
continue;
}
var user = userFeature.User;
var fullName = $"{user.FirstName} {user.LastName}".Trim();
if (string.IsNullOrWhiteSpace(fullName)) fullName = user.Mobile;
// کال کردن API چتیکا با retry
var result = await _retryPipeline.ExecuteAsync(
async ct => await _chatikaApiService.CreateAccountAsync(
user.Mobile,
fullName,
ct),
cancellationToken);
if (result.IsSuccess)
{
// آپدیت فیچر با توضیحات و URL
var description = ChatikaFeatureDescription;
if (!string.IsNullOrEmpty(result.AccessUrl))
{
description = description.Replace(
"https://chatika.ir",
result.AccessUrl);
}
userFeature.Notes = description;
userFeature.IsActive = true;
userFeature.LastModified = DateTime.Now;
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"✅ Chatika account activated for user {UserId}",
userFeature.UserId);
successCount++;
}
else
{
_logger.LogWarning(
"⚠️ Failed to create Chatika account for user {UserId}: {Error}",
userFeature.UserId,
result.ErrorMessage);
failCount++;
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"❌ Error processing Chatika activation for user {UserId}",
userFeature.UserId);
failCount++;
}
}
_logger.LogInformation(
"🏁 [{ExecutionId}] Chatika activation job completed. Success: {Success}, Failed: {Failed}",
executionId,
successCount,
failCount);
}
catch (Exception ex)
{
_logger.LogError(ex,
"❌ [{ExecutionId}] Chatika activation job failed",
executionId);
throw;
}
}
}
@@ -92,9 +92,19 @@ public static class ConfigureServices
// Commission Calculation Strategy Factory - برای سوییچ بین ORM و SP
services.AddScoped<ICommissionCalculationStrategyFactory, CommissionCalculationStrategyFactory>();
// Chatika API Service - سرویس ایجاد حساب چتیکا
services.AddHttpClient<IChatikaApiService, ChatikaApiService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.ConfigureHttpClient((sp, client) =>
{
var config = sp.GetRequiredService<IConfiguration>();
client.Timeout = TimeSpan.FromSeconds(30);
});
// Background Workers - Deprecated: Using Hangfire instead
// services.AddHostedService<WeeklyNetworkCommissionWorker>();
services.AddScoped<WeeklyCommissionJob>(); // Hangfire Job (Scoped for DI)
services.AddScoped<ChatikaAccountActivationJob>(); // Hangfire Job for Chatika activation
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
{
@@ -0,0 +1,156 @@
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using CMSMicroservice.Application.Common.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Infrastructure.Services;
/// <summary>
/// پیاده‌سازی سرویس چتیکا با HttpClient
/// API: POST /api/v1/organizations/register-user
/// </summary>
public class ChatikaApiService : IChatikaApiService
{
private readonly HttpClient _httpClient;
private readonly ILogger<ChatikaApiService> _logger;
private readonly string _baseUrl;
private readonly string _apiKey;
public ChatikaApiService(
HttpClient httpClient,
ILogger<ChatikaApiService> logger,
IConfiguration configuration)
{
_httpClient = httpClient;
_logger = logger;
// تنظیمات از appsettings.json
_baseUrl = configuration["Chatika:BaseUrl"] ?? "https://api.chatika.ir";
_apiKey = configuration["Chatika:ApiKey"] ?? "";
// تنظیم HttpClient
_httpClient.BaseAddress = new Uri(_baseUrl);
if (!string.IsNullOrEmpty(_apiKey))
{
_httpClient.DefaultRequestHeaders.Add("X-API-Key", _apiKey);
}
}
/// <inheritdoc />
public async Task<ChatikaAccountResult> CreateAccountAsync(
string mobileNumber,
string fullName,
CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation(
"🤖 Creating Chatika account for mobile: {Mobile}",
mobileNumber.Substring(0, 4) + "***");
var request = new ChatikaRegisterRequest
{
MobileNumber = mobileNumber
};
var response = await _httpClient.PostAsJsonAsync(
"/api/v1/organizations/register-user",
request,
cancellationToken);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<ChatikaRegisterResponse>(
cancellationToken: cancellationToken);
_logger.LogInformation(
"✅ Chatika account created successfully. UserId: {UserId}, IsNewUser: {IsNew}, Credit: {Credit}",
result?.Id,
result?.IsNewUser,
result?.CreditCharged);
return ChatikaAccountResult.Success(
chatikaUserId: result?.Id.ToString(),
accessUrl: "https://chatika.ir"
);
}
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
var errorResponse = JsonSerializer.Deserialize<ChatikaErrorResponse>(errorContent);
_logger.LogWarning(
"⚠️ Chatika API returned error. Status: {Status}, Code: {ErrorCode}, Message: {Message}",
response.StatusCode,
errorResponse?.ErrorCode,
errorResponse?.Message);
return ChatikaAccountResult.Failure($"{errorResponse?.ErrorCode}: {errorResponse?.Message}");
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "❌ Network error calling Chatika API");
return ChatikaAccountResult.Failure($"خطای شبکه: {ex.Message}");
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, "❌ Timeout calling Chatika API");
return ChatikaAccountResult.Failure("تایم‌اوت در ارتباط با سرویس چتیکا");
}
catch (Exception ex)
{
_logger.LogError(ex, "❌ Unexpected error calling Chatika API");
return ChatikaAccountResult.Failure($"خطای غیرمنتظره: {ex.Message}");
}
}
/// <summary>
/// Request model برای ثبت کاربر در چتیکا
/// </summary>
private class ChatikaRegisterRequest
{
[JsonPropertyName("mobile_number")]
public string MobileNumber { get; set; } = string.Empty;
}
/// <summary>
/// Response model موفق از چتیکا
/// </summary>
private class ChatikaRegisterResponse
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("mobile_number")]
public string MobileNumber { get; set; } = string.Empty;
[JsonPropertyName("organization_id")]
public long OrganizationId { get; set; }
[JsonPropertyName("organization_title")]
public string OrganizationTitle { get; set; } = string.Empty;
[JsonPropertyName("wallet_balance")]
public decimal WalletBalance { get; set; }
[JsonPropertyName("is_new_user")]
public bool IsNewUser { get; set; }
[JsonPropertyName("credit_charged")]
public decimal CreditCharged { get; set; }
}
/// <summary>
/// Response model خطا از چتیکا
/// </summary>
private class ChatikaErrorResponse
{
[JsonPropertyName("error_code")]
public string ErrorCode { get; set; } = string.Empty;
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
}
}
+8
View File
@@ -223,6 +223,14 @@ using (var scope = app.Services.CreateScope())
// Daya Loan Check: Every 15 minutes
CMSMicroservice.WebApi.Workers.DayaLoanCheckWorker.Schedule(recurringJobManager);
app.Logger.LogInformation("✅ Hangfire recurring job 'daya-loan-check' registered (Cron: */15 * * * * - Every 15 minutes)");
// Chatika Account Activation: Every 5 minutes
recurringJobManager.AddOrUpdate<CMSMicroservice.Infrastructure.BackgroundJobs.ChatikaAccountActivationJob>(
recurringJobId: "chatika-account-activation",
methodCall: job => job.ExecuteAsync(CancellationToken.None),
cronExpression: "*/5 * * * *",
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
app.Logger.LogInformation("✅ Hangfire recurring job 'chatika-account-activation' registered (Cron: */5 * * * * - Every 5 minutes)");
}
app.Run();
@@ -50,6 +50,10 @@
"MerchantPermissionKey": "56146364$04sXjethI5WxhItR1Q9xnmFdJzl2BB8Bclsq8dAy7YVSZp3vtt-wP7ivrcCvmKLq",
"CacheDurationMinutes": 20
},
"Chatika": {
"BaseUrl": "https://api.chatika.ir",
"ApiKey": "tIukvL8dnV4cB3yVWcCD9Xyfbj8rBxm5wPt2mLyJCgTsBBoMTWjt6mFEqQwpw-er"
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {