e3850f9dd8
- CHANGELOG: Phase 11 (11a-11f) — ZarinPal verify fix, تومان/ریال مدل, صفحه موفقیت, حذف ×۱۰ دوبار, callback URL امنیت - BUSINESS-02: تصحیح مدل ارزی (DB=تومان نه ریال), ZarinPal verify fix, جدول callback URL امنیت - TECH-01: اضافه CmsBaseUrl/FrontOfficeBaseUrl به appsettings, توضیح امنیت Open Redirect - TECH-02: اضافه PaymentCallback.razor, وضعیتهای جدید - ROADMAP: بروزرسانی Payment 97→99%, اضافه فاز ۱۱ به DONE list
320 lines
10 KiB
Markdown
320 lines
10 KiB
Markdown
# ⚙️ معماری CMS و زیرساخت فنی
|
||
|
||
> **منابع ادغامشده:** `CMS-README.md`, `ICURRENTUSERSERVICE-IMPLEMENTATION.md`, `FILE-MANAGEMENT-ARCHITECTURE.md`, `FRONTOFFICE-CMS-API-COMPATIBILITY.md`, `BFF-REMOVAL-PLAN.md`, `system-constants.md`
|
||
> **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: فیکس ZarinPal Verify + Callback URL امنیت + appsettings.Development.json)
|
||
|
||
---
|
||
|
||
## ۱. Stack فنی
|
||
|
||
| لایه | تکنولوژی | نسخه |
|
||
|------|----------|------|
|
||
| **Runtime** | .NET | 9.0 |
|
||
| **ORM** | Entity Framework Core | 9.0 |
|
||
| **Communication** | gRPC (Protobuf) | v3 |
|
||
| **Pattern** | CQRS + MediatR | — |
|
||
| **Database** | SQL Server (MSSQL) | 2022-CU16 |
|
||
| **Job Scheduler** | Hangfire | — |
|
||
| **Auth** | JWT Bearer + Identity | — |
|
||
| **API Gateway** | حذفشده (Direct gRPC) | — |
|
||
|
||
---
|
||
|
||
## ۲. معماری لایهای CMS
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
subgraph PRES["💻 Presentation Layer"]
|
||
FO["FrontOffice\nBlazor Server"]
|
||
BO["BackOffice\nBlazor WASM"]
|
||
end
|
||
|
||
FO & BO -->|gRPC| APP
|
||
|
||
subgraph APP["⚙️ Application Layer"]
|
||
CMD["Commands\nMediatR IRequest"]
|
||
QRY["Queries\nMediatR IRequest"]
|
||
VAL["Validators\nFluentValidation"]
|
||
HND["Handlers\nIRequestHandler"]
|
||
end
|
||
|
||
APP --> DOM
|
||
|
||
subgraph DOM["📦 Domain Layer"]
|
||
ENT["Entities, Enums\nValue Objects\nDomain Events"]
|
||
end
|
||
|
||
DOM --> INF
|
||
|
||
subgraph INF["🔧 Infrastructure Layer"]
|
||
EF["EF Core DbContext"]
|
||
SVC["External Services"]
|
||
HF["Hangfire Jobs"]
|
||
FS["File Storage"]
|
||
end
|
||
|
||
INF --> DB[("🗄️ SQL Server\nSchema: CMS")]
|
||
```
|
||
|
||
---
|
||
|
||
## ۳. CQRS با MediatR
|
||
|
||
### ۳.۱ ساختار فولدرها
|
||
|
||
```
|
||
CMS/src/
|
||
├── CMSMicroservice/
|
||
│ ├── Features/
|
||
│ │ ├── Products/
|
||
│ │ │ ├── Commands/
|
||
│ │ │ │ ├── CreateProductCommand.cs
|
||
│ │ │ │ └── CreateProductCommandHandler.cs
|
||
│ │ │ ├── Queries/
|
||
│ │ │ │ ├── GetProductsQuery.cs
|
||
│ │ │ │ └── GetProductsQueryHandler.cs
|
||
│ │ │ └── Validators/
|
||
│ │ │ └── CreateProductCommandValidator.cs
|
||
│ │ ├── Orders/
|
||
│ │ ├── Users/
|
||
│ │ ├── Club/
|
||
│ │ ├── Payment/
|
||
│ │ └── Blog/
|
||
│ ├── Services/
|
||
│ │ ├── gRPC/ ← gRPC service implementations
|
||
│ │ ├── Background/ ← Hangfire jobs
|
||
│ │ └── External/ ← ZarinPal, Kavenegar, Daya, Chatika
|
||
│ ├── Infrastructure/
|
||
│ │ ├── Persistence/ ← DbContext, Migrations
|
||
│ │ └── Identity/ ← JWT, Claims, ICurrentUserService
|
||
│ └── Protos/ ← .proto files
|
||
```
|
||
|
||
### ۳.۲ مثال Command
|
||
|
||
```csharp
|
||
// Command
|
||
public record CreateProductCommand(
|
||
string Name, string Description, decimal Price,
|
||
Guid CategoryId, string ImageUrl
|
||
) : IRequest<Guid>;
|
||
|
||
// Handler
|
||
public class CreateProductCommandHandler
|
||
: IRequestHandler<CreateProductCommand, Guid>
|
||
{
|
||
private readonly CMSDbContext _db;
|
||
|
||
public async Task<Guid> Handle(
|
||
CreateProductCommand request, CancellationToken ct)
|
||
{
|
||
var product = new Product { /* map fields */ };
|
||
_db.Products.Add(product);
|
||
|
||
// Auto-create inventory record
|
||
_db.Inventories.Add(new Inventory { ProductId = product.Id });
|
||
|
||
await _db.SaveChangesAsync(ct);
|
||
return product.Id;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## ۴. gRPC Services
|
||
|
||
### ۴.۱ لیست سرویسها
|
||
|
||
| سرویس | proto | متدهای اصلی |
|
||
|--------|-------|-------------|
|
||
| `ProductService` | product.proto | GetProducts, GetProduct, Create, Update, Delete |
|
||
| `OrderService` | order.proto | CreateOrder, GetOrders, UpdateStatus |
|
||
| `UserService` | user.proto | Register, Login, GetProfile, UpdateProfile |
|
||
| `ClubService` | club.proto | GetNetworkTree, GetBalance, AcceptContract |
|
||
| `PaymentService` | payment.proto | CreatePayment, VerifyPayment |
|
||
| `BlogService` | blog.proto | GetPosts, GetPost, Create, Update |
|
||
| `InventoryService` | inventory.proto | GetInventory, UpdateStock |
|
||
| `FileService` | file.proto | Upload, Download, Delete |
|
||
| `SitePageService` | sitepage.proto | GetPage, SaveSettings |
|
||
| `CategoryService` | category.proto | GetCategories, Create, Update |
|
||
| `SystemConfigService` | config.proto | GetConfig, UpdateConfig |
|
||
| `UserWalletService` | userwallet.proto | GetCustomerWallet, InitiateMagicCharge, GetMagicWalletStatus |
|
||
| `UserWalletHistoryService` | userwallethistory.proto | *(renamed from UserWalletChangeLogService)* |
|
||
|
||
### ۴.۲ PaginationState (مشترک)
|
||
|
||
```protobuf
|
||
message PaginationState {
|
||
int32 skip = 1;
|
||
int32 take = 2;
|
||
}
|
||
```
|
||
|
||
**Namespace صحیح:**
|
||
```csharp
|
||
using CMSMicroservice.Protobuf.Protos.PaginationState;
|
||
// ⚠️ نه: CMSMicroservice.Protobuf.Protos.PublicMessages.PaginationState
|
||
```
|
||
|
||
---
|
||
|
||
## ۵. Database
|
||
|
||
### ۵.۱ اتصال
|
||
|
||
```
|
||
Staging: Server=194.5.195.53; Database=FourSatCMS; Schema=CMS
|
||
Production: Server=45.149.79.127; Database=FourSatCMS; Schema=CMS
|
||
Engine: MSSQL 2022-CU16, Collation=Arabic_CI_AS
|
||
```
|
||
|
||
### ۵.۲ جداول اصلی
|
||
|
||
| جدول | توضیح | رکوردهای تقریبی |
|
||
|------|--------|----------------|
|
||
| Users | کاربران + فیلدهای شبکه (NetworkParentId, LegPosition) | ~5K |
|
||
| Products | محصولات (+ MaxDiscountPercent) | ~200 |
|
||
| Categories | دستهبندیها | ~30 |
|
||
| Orders | سفارشات | ~2K |
|
||
| Inventories | موجودی | ~200 |
|
||
| BlogPosts | پستهای بلاگ | ~50 |
|
||
| SitePages | صفحات سایت | ~10 |
|
||
| UserClubMemberships | عضویت باشگاه | ~500 |
|
||
| UserContracts | قراردادها (SignGuid, SignedPdfFile) | ~500 |
|
||
| UserWallets | کیفپول (Balance, NetworkBalance, DiscountBalance, WalletMode) | ~5K |
|
||
| ClubMembershipCycles | دورههای عضویت (CycleNumber, PackagePurchasedAt, IsCurrentCycle) | ~500 |
|
||
| Transactions | تراکنشها | ~5K |
|
||
| SystemConfigurations | تنظیمات | ~30 |
|
||
| ChatMessages | پیامهای چاتیکا | ~1K |
|
||
|
||
### ۵.۳ Stored Procedures
|
||
|
||
| SP | کاربرد |
|
||
|----|--------|
|
||
| `SP_GetNetworkTree` | بازگشتی — استخراج درخت باینری |
|
||
| `sp_CalculateWeeklyBalances` | محاسبه بالانس هفتگی هر عضو |
|
||
| `sp_CalculateWeeklyCommissionPool` | توزیع Pool هفتگی |
|
||
|
||
### ۵.۴ SP Auto-Deploy Worker (Q26)
|
||
|
||
```csharp
|
||
// StoredProcedureDeploymentService : IHostedService
|
||
// در startup:
|
||
// 1. خواندن فایلهای .sql از embedded resource
|
||
// 2. مقایسه checksum با جدول __SPChecksums
|
||
// 3. فقط SPهای تغییریافته re-deploy میشوند
|
||
```
|
||
|
||
---
|
||
|
||
## ۵.۵ History Tracking System (Q27)
|
||
|
||
### IHasHistory<T> Interface
|
||
|
||
```csharp
|
||
public interface IHasHistory<THistory> where THistory : BaseAuditableEntity, new()
|
||
{
|
||
THistory CreateHistorySnapshot(string action, string? performedBy);
|
||
}
|
||
```
|
||
|
||
### HistoryTrackingSaveChangesInterceptor
|
||
|
||
- **مکان:** `Infrastructure/Persistence/Interceptors/HistoryTrackingSaveChangesInterceptor.cs`
|
||
- **مکانیسم:** `SaveChangesInterceptor` — قبل از `SaveChanges` اجرا میشود
|
||
- **شناسایی:** از `ChangeTracker` entityهایی که `IHasHistory<>` پیادهسازی کردن (Modified/Added)
|
||
- **Auto-fill:** فیلدهای `Old*` از `entry.OriginalValues` با naming convention (مثلاً `OldPrice` ← `OriginalValues["Price"]`)
|
||
- **Entityهای فعال:** `Package` → `PackageHistory`
|
||
|
||
### History Tables
|
||
|
||
| جدول | Entity مرتبط | فیلدهای Old/New |
|
||
|------|-------------|----------------|
|
||
| `PackageHistories` | Package | Price, ActivationFee, MagicMultiplier, MagicMaxDeposit, MaxBalancesPerLeg, IsActive |
|
||
| `ClubMembershipCycleHistories` | ClubMembershipCycle | IsCurrentCycle, MagicStartedAt, MagicCompletedAt |
|
||
| `UserWalletHistories` | UserWallet | *(renamed from UserWalletChangeLogs — RenameTable migration)* |
|
||
|
||
---
|
||
|
||
## ۶. حذف BFF / Gateway
|
||
|
||
### ۶.۱ قبل vs بعد
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph BEFORE["قبل"]
|
||
F1["FrontOffice"] -->|REST| BFF1["BFF"]
|
||
B1["BackOffice"] -->|REST| BFF1
|
||
BFF1 -->|gRPC| C1["CMS"]
|
||
end
|
||
|
||
subgraph AFTER["بعد — فعلی ✅"]
|
||
F2["FrontOffice"] -->|gRPC| C2["CMS"]
|
||
B2["BackOffice"] -->|gRPC| C2
|
||
end
|
||
```
|
||
|
||
> مزایا: حذف لایه واسط → کاهش latency • Type-safe از proto تا UI • کاهش ۱ سرویس در deployment
|
||
|
||
### ۶.۲ سازگاری API
|
||
|
||
```
|
||
FrontOffice Service Layer:
|
||
• ProductService.cs → gRPC client wrapper
|
||
• OrderService.cs → gRPC client wrapper
|
||
• UserService.cs → gRPC client wrapper
|
||
|
||
هر Service:
|
||
• Constructor: inject GrpcChannel
|
||
• Methods: wrap gRPC calls + map to DTOs
|
||
• Error handling: try/catch RpcException
|
||
```
|
||
|
||
---
|
||
|
||
## ۷. Hangfire Jobs
|
||
|
||
| Job | فرکانس (cron) | کاربرد |
|
||
|-----|---------|--------|
|
||
| `WeeklyCommissionCalculation` | `5 0 * * 0` (یکشنبه ۰۰:۰۵) | محاسبه و توزیع کمیسیون |
|
||
| `DayaLoanProcessorJob` | `*/20 * * * *` (هر ۲۰ دقیقه) | پردازش درخواستهای وام |
|
||
| `ChatikaAccountActivation` | `*/5 * * * *` (هر ۵ دقیقه) | فعالسازی حساب چاتیکا |
|
||
|
||
---
|
||
|
||
## ۸. پیکربندی
|
||
|
||
### ۸.۱ appsettings.json ساختار
|
||
|
||
```json
|
||
{
|
||
"ConnectionStrings": {
|
||
"DefaultConnection": "Server=...;Database=FourSatCMS"
|
||
},
|
||
"Jwt": {
|
||
"Secret": "***",
|
||
"Issuer": "FourSat",
|
||
"ExpiryMinutes": 1440
|
||
},
|
||
"Grpc": {
|
||
"CmsUrl": "https://localhost:5001"
|
||
},
|
||
"Hangfire": {
|
||
"DashboardPath": "/hangfire",
|
||
"WorkerCount": 4
|
||
},
|
||
"Kavenegar": { "ApiKey": "***" },
|
||
"ZarinPal": { "MerchantId": "***", "UseSandbox": true },
|
||
"DayaLoan": { "UseMock": true },
|
||
"CmsBaseUrl": "https://cms.se.kbs1.ir",
|
||
"FrontOfficeBaseUrl": "http://localhost:5268"
|
||
}
|
||
```
|
||
|
||
> **⚠️ نکات مهم appsettings:**
|
||
> - `CmsBaseUrl` — برای callback URLهای درگاه (شارژ کیفپول جادویی/اعتباری)
|
||
> - `FrontOfficeBaseUrl` — برای redirect بعد پرداخت (خرید پکیج/تراکنش عمومی)
|
||
> - `appsettings.Development.json` — URLهای localhost برای توسعه محلی
|
||
> - همه callback URLها از config خوانده میشوند — هیچ URL از ورودی کاربر نمیآید (امنیت Open Redirect)
|