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 منتقل شد
🎯 اهداف پروژه
اهداف اولیه:
- حذف وابستگی به BFF: اتصال مستقیم FrontOffice به CMS
- حفظ Business Logic: "چیزی کم نشه از بیزینس"
- Customer API Pattern: متدهای Customer-prefix برای امنیت
- Nexus Integration: استفاده از Nexus برای package management
دلایل مهاجرت:
- کاهش پیچیدگی معماری (حذف یک لایه)
- بهبود عملکرد (کمتر شدن hop ها)
- کاهش maintenance overhead
- یکپارچهسازی با سایر microservice ها
📊 وضعیت اولیه پروژه
معماری قبلی:
FrontOffice → FrontOffice.BFF → CMS
پکیجهای استفاده شده قبلی:
FrontOffice.BFF.Package.ProtobufFrontOffice.BFF.ClubMembership.ProtobufFrontOffice.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/
تصمیمات معماری:
- Customer API Pattern: تمام متدهای عمومی با prefix
Customer - Field Aliasing: استفاده از field aliasing برای سازگاری با frontend
- 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
تغییرات:
- اضافه
payment_gateway_urlبهInitiateBasePackagePaymentResponse:
message InitiateBasePackagePaymentResponse {
bool success = 1;
string message = 2;
int64 order_id = 3;
string authority = 4;
string payment_gateway_url = 5;
}
- 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.razorPages/Profile/Components/AddAddressDialog.razor.csPages/Profile/Components/EditAddressDialog.razorPages/Profile/Components/EditAddressDialog.razor.cs
تغییرات کلیدی:
- حذف
_validator(FluentValidation) - Uncomment و پیادهسازی
SearchCities - استفاده از
object?برای_selectedCity - Cast به
CityDtoدر Razor templates - استفاده از
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 = 13networkmembership.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.PaginationStateCMSMicroservice.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
🔮 مراحل بعدی
توصیههای بهبود:
- Testing: اضافه کردن unit tests برای Customer APIs
- Documentation: Swagger/OpenAPI docs برای CMS
- Monitoring: اضافه کردن logging و metrics
- Security: بررسی authorization در Customer APIs
- Performance: اضافه کردن caching layer
- Migration: مهاجرت BackOffice به همین الگو
فایلهای نیاز به بررسی:
CheckoutSummary.razor- MudListItemText warningWeekSelector.razor- optimization opportunitiesOrganizationChart.razor- performance improvements
👥 مشارکتکنندگان
- توسعهدهنده اصلی: Masoud
- تاریخ شروع: فوریه 2026
- تاریخ اتمام: 2 فوریه 2026
- مدت زمان: چند ساعت (مهاجرت سیستماتیک)
📞 پشتیبانی
برای سوالات یا مشکلات:
- بررسی این مستند
- چک کردن error logs در CMS
- بررسی browser console در FrontOffice
- بررسی 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