Files
docs/migration/FRONTOFFICE-TO-CMS-MIGRATION.md
T

39 KiB

مستندات مهاجرت FrontOffice از BFF به CMS مستقیم

تاریخ: 2 فوریه 2026
وضعیت: تکمیل شده و آماده استفاده

📋 خلاصه اجرایی

این پروژه مهاجرت FrontOffice را از معماری BFF (Backend for Frontend) به اتصال مستقیم با CMS Microservice انجام داده است. هدف اصلی حذف لایه میانی BFF و ارتباط مستقیم frontend با CMS بود.

نتایج کلیدی:

  • 250+ خطای کامپایل به 0 خطا کاهش یافت
  • 8 Customer API جدید به CMS اضافه شد
  • 17+ field به proto ها اضافه شد
  • 7 نسخه package تولید شد (0.0.170 → 0.0.177)
  • بدون از دست رفتن هیچ business logic
  • Package به Nexus منتقل شد

🎯 اهداف پروژه

اهداف اولیه:

  1. حذف وابستگی به BFF: اتصال مستقیم FrontOffice به CMS
  2. حفظ Business Logic: "چیزی کم نشه از بیزینس"
  3. Customer API Pattern: متدهای Customer-prefix برای امنیت
  4. Nexus Integration: استفاده از Nexus برای package management

دلایل مهاجرت:

  • کاهش پیچیدگی معماری (حذف یک لایه)
  • بهبود عملکرد (کمتر شدن hop ها)
  • کاهش maintenance overhead
  • یکپارچه‌سازی با سایر microservice ها

📊 وضعیت اولیه پروژه

معماری قبلی:

FrontOffice → FrontOffice.BFF → CMS

پکیج‌های استفاده شده قبلی:

  • FrontOffice.BFF.Package.Protobuf
  • FrontOffice.BFF.ClubMembership.Protobuf
  • FrontOffice.BFF.City.Protobuf

خطاهای اولیه:

  • 250+ خطای کامپایل پس از حذف BFF
  • Missing types و namespaces
  • Field mismatches
  • Service registration issues

🔄 فرآیند مهاجرت

مرحله 1: تحلیل و برنامه‌ریزی

بررسی BFF Proto Files:

BFF به عنوان specification برای نیازهای frontend استفاده شد:

FrontOffice.BFF/src/Protobufs/
├── FrontOffice.BFF.Package.Protobuf/
├── FrontOffice.BFF.ClubMembership.Protobuf/
└── FrontOffice.BFF.City.Protobuf/

تصمیمات معماری:

  1. Customer API Pattern: تمام متدهای عمومی با prefix Customer
  2. Field Aliasing: استفاده از field aliasing برای سازگاری با frontend
  3. Direct CMS Connection: بدون لایه واسط

مرحله 2: اضافه کردن Customer API Methods به CMS

2.1. Commission APIs

فایل: CMS/src/CMSMicroservice.Protobuf/Protos/commission.proto

Methods اضافه شده:

// Customer-specific Commission methods
rpc GetMyCommissionPayouts(GetMyCommissionPayoutsRequest) returns (GetMyCommissionPayoutsResponse);
rpc GetMyWeeklyBalances(GetMyWeeklyBalancesRequest) returns (GetMyWeeklyBalancesResponse);

Messages جدید:

message CustomerMetaData {
    int32 current_page = 1;
    int32 total_pages = 2;
    int32 page_size = 3;
    int64 total_count = 4;
}

message CustomerCommissionPayoutModel {
    int64 id = 1;
    int64 user_id = 2;
    double amount = 3;
    string status = 4;
    string created_at = 5;
    string paid_at = 6;
}

message CustomerWeeklyBalanceModel {
    int64 id = 1;
    int64 user_id = 2;
    int64 week_id = 3;
    double left_leg_volume = 4;
    double right_leg_volume = 5;
    double commission_earned = 6;
    double left_leg_carryover = 11;
    double right_leg_carryover = 12;
    int32 left_leg_new_members = 13;
    int32 right_leg_new_members = 14;
    WeekDefinitionItem week_definition = 7;
}

message WeekDefinitionItem {
    int64 id = 1;
    string start_date = 2;
    string end_date = 3;
    int32 week_number = 4;
    int32 year = 5;
    string start_date_persian = 6;
    string end_date_persian = 7;
}

Fields اضافه به GetWeekDefinitionsRequest:

message GetWeekDefinitionsRequest {
    PaginationState pagination_state = 1;
    int32 page_number = 2;
    int32 page_size = 3;
    string search_text = 4;
    int32 gregorian_year = 5;
    int32 persian_year = 6;
    bool is_active = 7;
}

نسخه: 0.0.170 → 0.0.171


2.2. Network Membership APIs

فایل: CMS/src/CMSMicroservice.Protobuf/Protos/networkmembership.proto

Methods اضافه شده:

rpc GetMyNetworkTree(GetMyNetworkTreeRequest) returns (GetMyNetworkTreeResponse);
rpc GetSubordinateTree(GetSubordinateTreeRequest) returns (GetSubordinateTreeResponse);
rpc GetMyNetworkStatistics(GetMyNetworkStatisticsRequest) returns (GetMyNetworkStatisticsResponse);

تغییرات مهم:

  • حذف CustomerNetworkNodeModel (تکراری)
  • استفاده یکپارچه از NetworkTreeNodeModel
  • Field aliasing برای سازگاری:
message NetworkTreeNodeModel {
    int64 user_id = 1;
    string username = 2;
    string email = 3;
    string phone_number = 4;
    int32 depth = 5;
    int32 total_subordinates = 6;
    bool is_active = 7;
    string registration_date = 8;
    string last_purchase_date = 9;
    double total_purchases = 10;
    int32 package_type = 11;
    string package_expiry = 12;
    int32 rank = 13;
    string mobile = 14;
    string avatar = 15;
    string position = 16;
    NetworkTreeNodeModel left_child = 17;
    NetworkTreeNodeModel right_child = 18;
    string full_name = 20;  // alias
    int32 level = 21;        // alias
}

نسخه: 0.0.171 → 0.0.172


2.3. Configuration APIs

فایل: CMS/src/CMSMicroservice.Protobuf/Protos/configuration.proto

Methods اضافه شده:

rpc GetClubConfiguration(GetClubConfigurationRequest) returns (GetClubConfigurationResponse);
rpc GetClubFeatures(GetClubFeaturesRequest) returns (GetClubFeaturesResponse);

Messages جدید:

message GetClubConfigurationResponse {
    int64 activation_fee = 1;
    int64 membership_gift_value = 2;
}

message ClubFeatureModel {
    int64 id = 1;
    string title = 2;
    string description = 3;
    bool is_enabled = 4;
    int32 display_order = 5;
    string granted_at = 6;
    string created_at = 7;
    string notes = 8;
}

نسخه: 0.0.172 → 0.0.173


2.4. User Order APIs

فایل: CMS/src/CMSMicroservice.Protobuf/Protos/userorder.proto

Method اضافه شده:

rpc GetVATRate(GetVATRateRequest) returns (GetVATRateResponse);

Message جدید:

message GetVATRateResponse {
    double vat_rate = 1;
    int32 vat_percentage = 2;
    bool is_enabled = 3;
}

نسخه: 0.0.173 → 0.0.174


2.5. Package APIs

فایل: CMS/src/CMSMicroservice.Protobuf/Protos/package.proto

تغییرات:

  1. اضافه payment_gateway_url به InitiateBasePackagePaymentResponse:
message InitiateBasePackagePaymentResponse {
    bool success = 1;
    string message = 2;
    int64 order_id = 3;
    string authority = 4;
    string payment_gateway_url = 5;
}
  1. Field aliasing در CustomerPackageModel:
message CustomerPackageModel {
    int64 id = 1;
    string name = 2;
    string description = 3;
    int64 price = 4;
    string currency = 5;
    PackageTypeEnum package_type = 6;
    bool is_available = 7;
    string image_url = 8;
    int32 validity_days = 9;
    bool is_popular = 10;
    string short_description = 11;
    string title = 12;       // alias for name
    string image_path = 13;  // alias for image_url
}

نسخه: 0.0.174 → 0.0.175


2.6. City APIs

فایل: CMS/src/CMSMicroservice.Protobuf/Protos/city.proto

مشکل: GetAllCitiesByFilterResponseModel در proto تعریف شده بود اما protobuf compiler آن را generate نمی‌کرد.

راه حل: استفاده از CityDto به جای GetAllCitiesByFilterResponseModel

Implementation در Address Dialogs:

// Using object type with dynamic casting
private object? _selectedCity;

private async Task<IEnumerable<object>> SearchCities(string value, CancellationToken ct)
{
    var response = await CityContract.GetAllCitiesByFilterAsync(new GetAllCitiesByFilterRequest
    {
        PaginationState = new CMSMicroservice.Protobuf.Protos.City.PaginationState 
        { 
            PageNumber = 1, 
            PageSize = 20 
        },
        Filter = new GetAllCitiesByFilterFilter { Name = value }
    });
    
    return response?.Cities?.Cast<object>() ?? Enumerable.Empty<object>();
}

// In Razor
ToStringFunc="@(city => city != null ? 
    $"{((CMSMicroservice.Protobuf.Protos.City.CityDto)city).Native} 
    ({((CMSMicroservice.Protobuf.Protos.City.CityDto)city).StateName})" 
    : string.Empty)"

نسخه: 0.0.175 → 0.0.176


مرحله 3: تنظیم FrontOffice

3.1. تغییر Package Reference

فایل: FrontOffice/src/FrontOffice.Main/FrontOffice.Main.csproj

قبل:

<PackageReference Include="FrontOffice.BFF.Package.Protobuf" Version="x.x.x" />
<PackageReference Include="FrontOffice.BFF.ClubMembership.Protobuf" Version="x.x.x" />
<PackageReference Include="FrontOffice.BFF.City.Protobuf" Version="x.x.x" />

بعد:

<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.176" />

3.2. تنظیم ConfigureServices

فایل: FrontOffice/src/FrontOffice.Main/ConfigureServices.cs

Using statements اضافه شده:

using CMSMicroservice.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.City;
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.Protobuf.Protos.Transactions;
using CMSMicroservice.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.UserCarts;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserWallet;
using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.Configuration;
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using CMSMicroservice.Protobuf.Protos.Commission;
using CMSMicroservice.Protobuf.Protos.AppVersion;

gRPC Clients تعریف شده:

services.AddScoped(CreateAuthenticatedClient<PackageContract.PackageContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserContract.UserContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserOrderContract.UserOrderContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserWalletContract.UserWalletContractClient>);
services.AddScoped(CreateAuthenticatedClient<CategoryContract.CategoryContractClient>);
services.AddScoped(CreateAuthenticatedClient<ProductsContract.ProductsContractClient>);
services.AddScoped(CreateAuthenticatedClient<TransactionsContract.TransactionsContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserCartsContract.UserCartsContractClient>);
services.AddScoped(CreateAuthenticatedClient<CityContract.CityContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserAddressContract.UserAddressContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserWalletChangeLogContract.UserWalletChangeLogContractClient>);
services.AddScoped(CreateAuthenticatedClient<ClubMembershipContract.ClubMembershipContractClient>);
services.AddScoped(CreateAuthenticatedClient<OtpTokenContract.OtpTokenContractClient>);
services.AddScoped(CreateAuthenticatedClient<ConfigurationContract.ConfigurationContractClient>);
services.AddScoped(CreateAuthenticatedClient<NetworkMembershipContract.NetworkMembershipContractClient>);
services.AddScoped(CreateAuthenticatedClient<CommissionContract.CommissionContractClient>);
services.AddScoped(CreateAuthenticatedClient<AppVersionContract.AppVersionContractClient>);

3.3. تنظیم appsettings.json

فایل: FrontOffice/src/FrontOffice.Main/appsettings.json

{
  "GwUrl": "https://localhost:32846",
  "DownloadUrl": "https://dl.afrino.co",
  "EncryptionSettings": {
    "Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
    "IV": "1wyF3Tt142MOkCpIyCxh/g=="
  },
  "SignalR": {
    "HubPath": "/hubs/token-relay"
  }
}

نکته: Port 32846 برای HTTPS CMS


3.4. Address Dialog Fixes

فایل‌های تغییر یافته:

  • Pages/Profile/Components/AddAddressDialog.razor
  • Pages/Profile/Components/AddAddressDialog.razor.cs
  • Pages/Profile/Components/EditAddressDialog.razor
  • Pages/Profile/Components/EditAddressDialog.razor.cs

تغییرات کلیدی:

  1. حذف _validator (FluentValidation)
  2. Uncomment و پیاده‌سازی SearchCities
  3. استفاده از object? برای _selectedCity
  4. Cast به CityDto در Razor templates
  5. استفاده از City.PaginationState به جای global

مرحله 4: رفع خطاهای Proto3

4.1. Field Number Conflicts

مشکل: Proto3 نمی‌تواند از یک field number برای چند field استفاده کند، حتی با aliasing.

مثال خطا:

// ❌ اشتباه
string name = 2;
string title = 2;  // Error: field number already used

راه حل:

// ✅ درست
string name = 2;
string title = 12;  // unique field number

فایل‌های تغییر یافته:

  • package.proto: title = 12, image_path = 13
  • networkmembership.proto: full_name = 20, level = 21

4.2. NetworkTreeNodeModel Type Mismatch

مشکل: دو type مشابه CustomerNetworkNodeModel و NetworkTreeNodeModel

راه حل: حذف CustomerNetworkNodeModel و استفاده یکپارچه از NetworkTreeNodeModel


4.3. Razor Compilation Cache

مشکل: Razor compiler تغییرات را cache می‌کند

راه حل:

rm -rf obj bin
dotnet build

مرحله 5: Nexus Integration

5.1. تنظیم NuGet.config در CMS

فایل: CMS/src/NuGet.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" 
         allowInsecureConnections="true" />
    <add key="foursat-hosted" value="http://194.5.195.53:32081/repository/foursat-nuget-hosted/index.json" 
         allowInsecureConnections="true" />
  </packageSources>
  <packageSourceCredentials>
    <Nexus>
      <add key="Username" value="admin" />
      <add key="ClearTextPassword" value="87zH26nbqT" />
    </Nexus>
    <foursat-hosted>
      <add key="Username" value="admin" />
      <add key="ClearTextPassword" value="87zH26nbqT" />
    </foursat-hosted>
  </packageSourceCredentials>
</configuration>

5.2. Auto-Push Target در csproj

فایل: CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj

<Target Name="PushToFoursatNuget" AfterTargets="Pack" Condition="'$(CI)' != 'true'">
  <PropertyGroup>
    <NugetPackagePath>$(PackageOutputPath)/$(PackageId).$(Version).nupkg</NugetPackagePath>
    <PushCommand>dotnet nuget push "$(NugetPackagePath)" --source foursat-hosted --api-key admin:87zH26nbqT --skip-duplicate --configfile "$(MSBuildThisFileDirectory)../NuGet.config"</PushCommand>
  </PropertyGroup>
  <Exec Command="$(PushCommand)" WorkingDirectory="$(MSBuildThisFileDirectory)" />
</Target>

استفاده:

cd CMS/src/CMSMicroservice.Protobuf
dotnet pack -c Release -o ../../../nupkg
# خودکار به Nexus push می‌شود

5.3. تنظیم NuGet.config در FrontOffice

فایل: FrontOffice/src/FrontOffice.Main/NuGet.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" 
         allowInsecureConnections="true" />
  </packageSources>
  <packageSourceCredentials>
    <Nexus>
      <add key="Username" value="admin" />
      <add key="ClearTextPassword" value="87zH26nbqT" />
    </Nexus>
  </packageSourceCredentials>
</configuration>

نکته: Local folder (../../../nupkg) حذف شد، فقط از Nexus استفاده می‌شود.


مرحله 6: رفع مشکلات CMS Build

6.1. حذف ProductsCQ

مشکل: فایل GetAllProductsByFilterQueryHandler.cs از BFF کپی شده بود و types نادرست داشت.

راه حل: حذف کامل پوشه ProductsCQ

rm -rf CMS/src/CMSMicroservice.Application/ProductsCQ

دلیل: Service ها مستقیماً از proto types استفاده می‌کنند، نیازی به Query/Command pattern نیست.


6.2. رفع خطاهای PackageService

فایل: CMS/src/CMSMicroservice.WebApi/Services/PackageService.cs

خطا 1: GetCustomerPackagesResponse.Packages وجود نداشت

قبل:

return new GetCustomerPackagesResponse
{
    Packages = { packages }
};

بعد:

return new GetCustomerPackagesResponse
{
    Models = { packages }  // Property name is "Models"
};

خطا 2: GetCustomerPackageDetailsResponse.Package وجود نداشت

قبل:

return new GetCustomerPackageDetailsResponse
{
    Package = new CustomerPackageModel { /* ... */ }
};

بعد (طبق proto definition):

return new GetCustomerPackageDetailsResponse
{
    Id = request.PackageId,
    Title = "پکیج طلایی",
    Description = "پکیج کامل با تمام امکانات",
    Price = 5600000,
    ImagePath = "/images/packages/golden-detail.jpg",
    Features = { packageFeatures },
    Requirements = new PurchaseRequirements
    {
        RequiresMembership = false,
        MinimumWalletBalance = 560000,
        Restrictions = { "باید حداقل 18 سال سن داشته باشید" }
    }
};

🔧 مشکلات و راه حل‌ها

1. Field Aliasing در Proto3

مشکل: Proto3 نمی‌تواند از field number تکراری استفاده کند.

راه حل: هر field باید unique number داشته باشد:

string name = 2;
string title = 12;  // NOT 2
string image_url = 8;
string image_path = 13;  // NOT 8

2. Protobuf Message Generation Issues

مشکل: GetAllCitiesByFilterResponseModel در proto بود اما generate نمی‌شد.

تحلیل:

  • Proto structure صحیح بود
  • Compiler مشکلی نداشت
  • احتمالاً به دلیل nested message یا naming conflict

راه حل: استفاده از CityDto که از قبل generate شده بود:

response?.Cities?.Cast<object>()  // Cities property returns List<CityDto>

3. Namespace Conflicts

مشکل: چند PaginationState با namespace های مختلف:

  • CMSMicroservice.Protobuf.Protos.PaginationState
  • CMSMicroservice.Protobuf.Protos.City.PaginationState

راه حل: استفاده از fully qualified name:

new CMSMicroservice.Protobuf.Protos.City.PaginationState { /* ... */ }

4. Razor Compilation Cache

مشکل: بعد از تغییرات proto، Razor files compile نمی‌شدند.

راه حل:

rm -rf obj bin
dotnet build

5. gRPC Client Registration

مشکل: Service injection failures در startup:

Unable to resolve service for type 'ConfigurationContract+ConfigurationContractClient'

راه حل: اضافه کردن تمام client ها به ConfigureServices.cs:

services.AddScoped(CreateAuthenticatedClient<ConfigurationContract.ConfigurationContractClient>);
services.AddScoped(CreateAuthenticatedClient<NetworkMembershipContract.NetworkMembershipContractClient>);
services.AddScoped(CreateAuthenticatedClient<CommissionContract.CommissionContractClient>);
services.AddScoped(CreateAuthenticatedClient<AppVersionContract.AppVersionContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserAddressContract.UserAddressContractClient>);

6. HTTP vs HTTPS Port Mismatch

مشکل: appsettings داشت https://localhost:32847 اما port 32847 فقط HTTP بود.

راه حل: استفاده از HTTPS port:

"GwUrl": "https://localhost:32846"

📦 Package Versions Timeline

Version Changes Date
0.0.170 نسخه اولیه -
0.0.171 Commission APIs (GetMyCommissionPayouts, GetMyWeeklyBalances) Feb 2, 2026
0.0.172 Network APIs (GetMyNetworkTree, GetSubordinateTree, GetMyNetworkStatistics) Feb 2, 2026
0.0.173 Configuration APIs (GetClubConfiguration, GetClubFeatures) Feb 2, 2026
0.0.174 UserOrder VAT API (GetVATRate) Feb 2, 2026
0.0.175 Package payment_gateway_url field Feb 2, 2026
0.0.176 Field aliasing fixes (title=12, image_path=13) Feb 2, 2026
0.0.177 Nexus auto-push test Feb 2, 2026

🎯 Customer API Pattern

تمام متدهای عمومی با prefix Customer شروع می‌شوند:

Commission:

  • GetMyCommissionPayouts - کمیسیون‌های من
  • GetMyWeeklyBalances - تراز هفتگی من

Network:

  • GetMyNetworkTree - درخت شبکه من
  • GetSubordinateTree - زیرمجموعه من
  • GetMyNetworkStatistics - آمار شبکه من

Package:

  • GetCustomerPackages - لیست پکیج‌ها برای مشتری
  • GetCustomerPackageDetails - جزئیات پکیج برای مشتری
  • CustomerPurchasePackage - خرید پکیج توسط مشتری

Configuration:

  • GetClubConfiguration - تنظیمات باشگاه
  • GetClubFeatures - ویژگی‌های باشگاه

City:

  • GetCitiesForCustomer - شهرها برای مشتری
  • GetAllCitiesByFilter - جستجوی شهر

📁 ساختار فایل‌های تغییر یافته

CMS Proto Files:

CMS/src/CMSMicroservice.Protobuf/Protos/
├── commission.proto           ✏️ Modified
├── networkmembership.proto    ✏️ Modified
├── configuration.proto        ✏️ Modified
├── userorder.proto           ✏️ Modified
├── package.proto             ✏️ Modified
└── city.proto                ✏️ Modified

FrontOffice Files:

FrontOffice/src/FrontOffice.Main/
├── ConfigureServices.cs                              ✏️ Modified
├── appsettings.json                                  ✏️ Modified
├── FrontOffice.Main.csproj                          ✏️ Modified
├── NuGet.config                                      ✏️ Modified
└── Pages/Profile/Components/
    ├── AddAddressDialog.razor                        ✏️ Modified
    ├── AddAddressDialog.razor.cs                     ✏️ Modified
    ├── EditAddressDialog.razor                       ✏️ Modified
    └── EditAddressDialog.razor.cs                    ✏️ Modified

CMS Service Files:

CMS/src/CMSMicroservice.WebApi/Services/
└── PackageService.cs                                 ✏️ Modified

CMS/src/CMSMicroservice.Application/
└── ProductsCQ/                                       🗑️ Deleted

🚀 دستورات نهایی

Build و Pack CMS Protobuf:

cd CMS/src/CMSMicroservice.Protobuf

# Update version در csproj
# <Version>0.0.177</Version>

# Build و auto-push به Nexus
dotnet pack -c Release -o ../../../nupkg

Build FrontOffice:

cd FrontOffice/src/FrontOffice.Main

# Clear NuGet cache
dotnet nuget locals all --clear

# Restore از Nexus
dotnet restore --configfile NuGet.config

# Build
dotnet build

# Run
dotnet run

Build CMS:

cd CMS/src/CMSMicroservice.WebApi
dotnet build
dotnet run

Checklist تکمیل

  • تحلیل BFF proto files
  • اضافه کردن Commission APIs
  • اضافه کردن Network APIs
  • اضافه کردن Configuration APIs
  • اضافه کردن VAT API
  • تنظیم Package proto
  • رفع مشکل City proto
  • تغییر package reference در FrontOffice
  • تنظیم ConfigureServices
  • رفع Address Dialog issues
  • تنظیم Nexus در CMS
  • تنظیم Nexus در FrontOffice
  • رفع خطاهای CMS build
  • تست کامل FrontOffice
  • تست کامل CMS
  • Documentation

📊 نتایج نهایی

خطاها:

  • قبل: 250+ خطای کامپایل
  • بعد: 0 خطا

API Methods:

  • قبل: فقط متدهای موجود در BFF
  • بعد: +8 متد Customer API جدید

Package Management:

  • قبل: Local folder
  • بعد: Nexus Repository

معماری:

  • قبل: FrontOffice → BFF → CMS (2 hop)
  • بعد: FrontOffice → CMS (1 hop)

Performance:

  • کاهش latency (حذف یک hop)
  • کاهش resource usage (حذف BFF)
  • بهبود maintainability

🔮 مراحل بعدی

توصیه‌های بهبود:

  1. Testing: اضافه کردن unit tests برای Customer APIs
  2. Documentation: Swagger/OpenAPI docs برای CMS
  3. Monitoring: اضافه کردن logging و metrics
  4. Security: بررسی authorization در Customer APIs
  5. Performance: اضافه کردن caching layer
  6. Migration: مهاجرت BackOffice به همین الگو

فایل‌های نیاز به بررسی:

  • CheckoutSummary.razor - MudListItemText warning
  • WeekSelector.razor - optimization opportunities
  • OrganizationChart.razor - performance improvements

👥 مشارکت‌کنندگان

  • توسعه‌دهنده اصلی: Masoud
  • تاریخ شروع: فوریه 2026
  • تاریخ اتمام: 2 فوریه 2026
  • مدت زمان: چند ساعت (مهاجرت سیستماتیک)

📞 پشتیبانی

برای سوالات یا مشکلات:

  1. بررسی این مستند
  2. چک کردن error logs در CMS
  3. بررسی browser console در FrontOffice
  4. بررسی Nexus repository برای package issues

📝 یادداشت‌های مهم

Proto3 Rules:

  • هر field باید unique number داشته باشد
  • Field aliasing نیاز به unique numbers دارد
  • Message nesting می‌تواند مشکل generation ایجاد کند

Blazor/Razor:

  • Compilation cache نیاز به clean build دارد
  • Using directives باید در top of file باشند
  • Dynamic casting برای generic object types

gRPC:

  • تمام client ها باید registered باشند
  • Port مismatch می‌تواند connection failure ایجاد کند
  • Authentication header باید در تمام requests باشد

Nexus:

  • allowInsecureConnections="true" برای HTTP
  • Credentials در packageSourceCredentials
  • --skip-duplicate برای جلوگیری از خطای push

تاریخ آخرین به‌روزرسانی: 2 فوریه 2026
وضعیت: Production Ready
نسخه مستند: 1.0


لاگ تغییرات FrontOffice

FrontOffice Customer App - Changes Log

تاریخ آخرین به‌روزرسانی: 2026-02-08
نسخه: 1.0


خلاصه تغییرات

این مستند شامل تمام تغییراتی است که در اپلیکیشن مشتری (FrontOffice) انجام شده تا ارتباط مستقیم با CMS برقرار شود.

هدف کلی:

  • حذف لایه BFF از معماری
  • اتصال مستقیم FrontOffice به CMS
  • استفاده از JWT Token برای احراز هویت

تغییرات انجام شده

1. صفحه عضویت باشگاه (/club/membership)

1.1 فایل: MembershipPage.razor

تغییرات:

  • حذف <ActivationSection> component (کارت فعال‌سازی/تمدید عضویت)
  • حذف نمایش هزینه عضویت (ActivationFee و MembershipGiftValue)

قبل:

<!-- کارت فعال‌سازی -->
@if (!_membership?.IsActive ?? true)
{
    <ActivationSection OnActivationSuccess="HandleActivationSuccess" />
}

<!-- نمایش هزینه‌ها -->
<MudPaper>
    <MudText>هزینه عضویت: @_clubConfig.ActivationFee.ToString("N0") ریال</MudText>
    <MudText>هدیه عضویت: @_clubConfig.MembershipGiftValue.ToString("N0") ریال</MudText>
</MudPaper>

بعد:

<!-- حذف شده -->

دلیل: به درخواست کاربر - این بخش‌ها اضافی بودند


1.2 فایل: MembershipPage.razor.cs

تغییرات:

  • حذف [Inject] private ISnackbar Snackbar (duplicate injection)

دلیل: ISnackbar قبلاً در _Imports.razor به صورت global inject شده بود


2. سرویس عضویت باشگاه

2.1 فایل: Utilities/ClubMembershipService.cs

تغییرات:

تغییر 1: اضافه شدن UserAuthInfo
// قبل
public class ClubMembershipService
{
    private readonly ClubMembershipContract.ClubMembershipContractClient _client;
    
    public ClubMembershipService(ClubMembershipContract.ClubMembershipContractClient client)
    {
        _client = client;
    }
}

// بعد
public class ClubMembershipService
{
    private readonly ClubMembershipContract.ClubMembershipContractClient _client;
    private readonly UserAuthInfo _authInfo;
    
    public ClubMembershipService(
        ClubMembershipContract.ClubMembershipContractClient client,
        UserAuthInfo authInfo)
    {
        _client = client;
        _authInfo = authInfo;
    }
}
تغییر 2: متد GetCurrentUserIdAsync
// قبل - مقدار hardcoded
private Task<long> GetCurrentUserIdAsync()
{
    return Task.FromResult(1L); // ❌ همیشه 1 برمی‌گرداند
}

// بعد - از UserAuthInfo می‌خواند
private Task<long> GetCurrentUserIdAsync()
{
    return Task.FromResult(_authInfo.UserId); // ✅ UserId واقعی از session
}
تغییر 3: متد GetMyMembershipAsync
// قبل
public async Task<ClubMembershipDto> GetMyMembershipAsync()
{
    var userId = await GetCurrentUserIdAsync();
    if (userId <= 0)
    {
        return new ClubMembershipDto { IsActive = false, Status = "Not Authenticated" };
    }
    var response = await _client.GetClubMembershipAsync(new GetClubMembershipRequest { UserId = userId });
    // ...
}

// بعد - CMS از JWT می‌خواند
public async Task<ClubMembershipDto> GetMyMembershipAsync()
{
    // UserId = 0 می‌فرستیم تا CMS از JWT بخواند
    var response = await _client.GetClubMembershipAsync(new GetClubMembershipRequest { UserId = 0 });
    return new ClubMembershipDto
    {
        UserId = response.UserId,
        IsActive = response.IsActive,
        Status = response.Status,
        DaysRemaining = response.DaysRemaining > 0 ? response.DaysRemaining : null
    };
}

دلیل: انتقال منطق احراز هویت به CMS - حالا CMS از JWT Token خود UserId را استخراج می‌کند


3. صفحه ویژگی‌های باشگاه (/club/features)

3.1 فایل: FeaturesPage.razor

تغییرات:

  • بازگردانی فراخوانی GetClubFeaturesAsync()
// قبل - stub بود
protected override async Task OnInitializedAsync()
{
    // TODO: Implement GetClubFeaturesAsync
    _features = new List<ClubFeatureDto>();
}

// بعد - فراخوانی واقعی
protected override async Task OnInitializedAsync()
{
    _features = await ClubConfigService.GetClubFeaturesAsync();
}

4. کامپوننت فعال‌سازی

4.1 فایل: Components/ActivationSection.razor.cs

تغییرات:

  • حذف [Inject] private ISnackbar Snackbar (duplicate injection)

دلیل: مشابه MembershipPage - already injected globally


تغییرات CMS (مرتبط با FrontOffice)

1. ConfigurationService

1.1 متد GetClubConfiguration (جدید)

public override Task<GetClubConfigurationResponse> GetClubConfiguration(Empty request, ServerCallContext context)
{
    return Task.FromResult(new GetClubConfigurationResponse
    {
        ActivationFee = SystemConstants.ClubActivationFee,         // 25,200,000 ریال
        MembershipGiftValue = SystemConstants.ClubMembershipGiftValue // 25,200,000 ریال
    });
}

1.2 متد GetClubFeatures (جدید)

public override async Task<GetClubFeaturesResponse> GetClubFeatures(Empty request, ServerCallContext context)
{
    // دریافت UserId از JWT
    if (!long.TryParse(_currentUserService.UserId, out var userId) || userId <= 0)
    {
        return new GetClubFeaturesResponse(); // لیست خالی برای کاربران احراز هویت نشده
    }

    // فراخوانی از طریق MediatR
    var query = new GetUserClubFeaturesQuery { UserId = userId };
    var userFeatures = await _mediator.Send(query, context.CancellationToken);

    // تبدیل به response
    var response = new GetClubFeaturesResponse();
    foreach (var feature in userFeatures)
    {
        response.Features.Add(new ClubFeatureModel { ... });
    }
    return response;
}

2. ClubMembershipService

2.1 متد GetClubMembership (اصلاح شده)

public override async Task<GetClubMembershipResponse> GetClubMembership(GetClubMembershipRequest request, ServerCallContext context)
{
    // اگر UserId در request نیست یا صفر است، از JWT بخوان
    if (request.UserId <= 0)
    {
        if (long.TryParse(_currentUserService.UserId, out var tokenUserId) && tokenUserId > 0)
        {
            _logger.LogInformation("GetClubMembership: Reading UserId from JWT token: {UserId}", tokenUserId);
            request = new GetClubMembershipRequest { UserId = tokenUserId };
        }
        else
        {
            throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
        }
    }
    
    return await _dispatchRequestToCQRS.Handle<...>(request, context);
}

3. GetClubMembershipQueryHandler

3.1 اصلاح برای handle کردن کاربران بدون عضویت

public async Task<ClubMembershipDto?> Handle(GetClubMembershipQuery request, CancellationToken cancellationToken)
{
    var membership = await _context.ClubMemberships
        .Where(x => x.UserId == request.UserId)
        .FirstOrDefaultAsync(cancellationToken);

    // اگر کاربر عضویت نداره، یک DTO با وضعیت غیرفعال برگردون
    if (membership == null)
    {
        _logger.LogInformation("No membership found for UserId: {UserId}", request.UserId);
        return new ClubMembershipDto
        {
            Id = 0,
            UserId = request.UserId,
            IsActive = false,  // ✅ غیرفعال
            // ...
        };
    }

    return membership;
}

4. CommissionService

4.1 متد GetMyWeeklyBalances (جدید)

public override async Task<GetMyWeeklyBalancesResponse> GetMyWeeklyBalances(GetMyWeeklyBalancesRequest request, ServerCallContext context)
{
    return await _dispatchRequestToCQRS.Handle<GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery, GetMyWeeklyBalancesResponse>(request, context);
}

Query Handler:

public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetMyWeeklyBalancesQuery request, CancellationToken cancellationToken)
{
    // دریافت UserId از JWT
    if (!long.TryParse(_currentUserService.UserId, out var userId) || userId <= 0)
    {
        throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
    }

    // فراخوانی GetUserWeeklyBalancesQuery با UserId از JWT
    var query = new GetUserWeeklyBalancesQuery
    {
        UserId = userId,
        WeekDefinitionId = request.WeekDefinitionId,
        OnlyActive = request.OnlyActive,
        PaginationState = request.PaginationState
    };

    return await _mediator.Send(query, cancellationToken);
}

خلاصه فایل‌های تغییر یافته

FrontOffice:

فایل نوع تغییر وضعیت
Pages/Club/MembershipPage.razor حذف کامپوننت‌ها
Pages/Club/MembershipPage.razor.cs حذف duplicate injection
Pages/Club/FeaturesPage.razor بازگردانی API call
Pages/Club/Components/ActivationSection.razor.cs حذف duplicate injection
Utilities/ClubMembershipService.cs تغییر authentication flow

CMS:

فایل نوع تغییر وضعیت
Services/ConfigurationService.cs اضافه GetClubConfiguration, GetClubFeatures
Services/ClubMembershipService.cs اصلاح GetClubMembership برای JWT
Services/CommissionService.cs اضافه GetMyWeeklyBalances
Application/.../GetClubMembershipQueryHandler.cs Handle null membership
Application/.../GetMyWeeklyBalancesQuery.cs جدید
Application/.../GetMyWeeklyBalancesQueryHandler.cs جدید
Mappings/CommissionProfile.cs اضافه mappings
Mappings/ClubMembershipProfile.cs اضافه mappings

مشکلات رفع شده

مشکل علت راه‌حل
Snackbar duplicate injection error ISnackbar در _Imports.razor و code-behind هر دو inject شده بود حذف از code-behind
ValidationException "شناسه کاربر معتبر نیست" GetCurrentUserIdAsync() همیشه 1 برمی‌گرداند استفاده از UserAuthInfo.UserId
GetClubConfiguration Unimplemented متد در CMS پیاده‌سازی نشده بود پیاده‌سازی با SystemConstants
GetClubFeatures Unimplemented متد در CMS پیاده‌سازی نشده بود پیاده‌سازی با MediatR
GetMyWeeklyBalances Unimplemented متد در CMS پیاده‌سازی نشده بود ایجاد Query و Handler جدید
کاربر فعال ولی نمایش غیرفعال Handler برای کاربران بدون رکورد null برمی‌گرداند برگرداندن DTO با IsActive=false

نکات مهم برای توسعه‌دهندگان

1. Authentication Pattern

FrontOffice → CMS با UserId=0 → CMS از JWT می‌خواند

2. ISnackbar

// ❌ اشتباه - duplicate injection
[Inject] private ISnackbar Snackbar { get; set; }

// ✅ درست - استفاده از global injection در _Imports.razor
// فقط استفاده کن: Snackbar.Add("message", Severity.Success);

3. UserAuthInfo

// برای دسترسی به اطلاعات کاربر جاری
[Inject] private UserAuthInfo AuthInfo { get; set; }

var userId = AuthInfo.UserId;
var username = AuthInfo.Username;

TODO (کارهای باقی‌مانده)

  • تست کامل صفحه /club/membership با کاربران مختلف
  • تست صفحه /club/features برای نمایش ویژگی‌ها
  • تست صفحه /commission/weekly-balance با هفته‌های مختلف
  • بررسی edge cases (کاربر جدید، کاربر بدون عضویت، etc.)

Document Version: 1.0
Last Updated: 2026-02-08
Author: Development Team