Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62a247f097 | |||
| bc710fdb7b | |||
| fd24dcebcd | |||
| 89e146bf32 |
@@ -80,6 +80,10 @@ public interface IApplicationDbContext
|
||||
DbSet<SitePage> SitePages { get; }
|
||||
DbSet<SitePageSection> SitePageSections { get; }
|
||||
|
||||
// ============= Site Page Settings (Simplified) =============
|
||||
DbSet<SitePageSettings> SitePageSettingsEntities { get; }
|
||||
DbSet<SitePageImage> SitePageImages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures
|
||||
/// </summary>
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.SitePageSettingsCQ.Commands.DeletePageImage;
|
||||
|
||||
// ── Command ──
|
||||
public class DeletePageImageCommand : IRequest<Unit>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
|
||||
// ── Handler ──
|
||||
public class DeletePageImageCommandHandler : IRequestHandler<DeletePageImageCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public DeletePageImageCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(DeletePageImageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.SitePageImages
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken);
|
||||
|
||||
if (entity != null)
|
||||
{
|
||||
entity.IsDeleted = true; // Soft delete
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
using CMSMicroservice.Application.Common.FileManager;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Content;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageImage;
|
||||
|
||||
// ── Command ──
|
||||
public class SavePageImageCommand : IRequest<SavePageImageResult>
|
||||
{
|
||||
public long Id { get; set; } // 0 = ایجاد جدید
|
||||
public long SitePageSettingsId { get; set; }
|
||||
public string ImageGroup { get; set; } = default!;
|
||||
public string? Title { get; set; }
|
||||
public string? Subtitle { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? LinkUrl { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
// Image upload
|
||||
public byte[]? ImageFileBytes { get; set; }
|
||||
public string? ImageFileMime { get; set; }
|
||||
public string? ImageFileName { get; set; }
|
||||
}
|
||||
|
||||
public class SavePageImageResult
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
|
||||
// ── Handler ──
|
||||
public class SavePageImageCommandHandler : IRequestHandler<SavePageImageCommand, SavePageImageResult>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IFileManager _fileManager;
|
||||
|
||||
public SavePageImageCommandHandler(IApplicationDbContext context, IFileManager fileManager)
|
||||
{
|
||||
_context = context;
|
||||
_fileManager = fileManager;
|
||||
}
|
||||
|
||||
public async Task<SavePageImageResult> Handle(SavePageImageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
SitePageImage entity;
|
||||
|
||||
if (request.Id > 0)
|
||||
{
|
||||
// ویرایش
|
||||
entity = await _context.SitePageImages
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
|
||||
?? throw new Exception($"تصویر با شناسه {request.Id} یافت نشد");
|
||||
}
|
||||
else
|
||||
{
|
||||
// ایجاد جدید
|
||||
entity = new SitePageImage
|
||||
{
|
||||
SitePageSettingsId = request.SitePageSettingsId,
|
||||
ImageGroup = request.ImageGroup,
|
||||
ImagePath = string.Empty // placeholder — will be set below
|
||||
};
|
||||
_context.SitePageImages.Add(entity);
|
||||
}
|
||||
|
||||
entity.Title = request.Title;
|
||||
entity.Subtitle = request.Subtitle;
|
||||
entity.Description = request.Description;
|
||||
entity.LinkUrl = request.LinkUrl;
|
||||
entity.IconName = request.IconName;
|
||||
entity.SortOrder = request.SortOrder;
|
||||
entity.IsActive = request.IsActive;
|
||||
|
||||
// آپلود تصویر
|
||||
if (request.ImageFileBytes is { Length: > 0 })
|
||||
{
|
||||
var result = await _fileManager.UploadImageAsync(
|
||||
$"Images/SitePageSettings/{request.ImageGroup}",
|
||||
request.ImageFileBytes,
|
||||
request.ImageFileMime ?? "image/jpeg",
|
||||
request.ImageFileName,
|
||||
cancellationToken);
|
||||
|
||||
entity.ImagePath = result.Main.Path;
|
||||
entity.ThumbnailPath = result.Thumbnail.Path;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new SavePageImageResult { Id = entity.Id, Success = true };
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
using CMSMicroservice.Application.Common.FileManager;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Content;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageSettings;
|
||||
|
||||
// ── Command ──
|
||||
public class SavePageSettingsCommand : IRequest<SavePageSettingsResult>
|
||||
{
|
||||
public string PageKey { get; set; } = default!;
|
||||
public string Title { get; set; } = default!;
|
||||
public string? MetaDescription { get; set; }
|
||||
public string? HeroTitle { get; set; }
|
||||
public string? HeroSubtitle { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public string? SettingsJson { get; set; }
|
||||
|
||||
// Hero image upload
|
||||
public byte[]? ImageFileBytes { get; set; }
|
||||
public string? ImageFileMime { get; set; }
|
||||
public string? ImageFileName { get; set; }
|
||||
}
|
||||
|
||||
public class SavePageSettingsResult
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
|
||||
// ── Handler ──
|
||||
public class SavePageSettingsCommandHandler : IRequestHandler<SavePageSettingsCommand, SavePageSettingsResult>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IFileManager _fileManager;
|
||||
|
||||
public SavePageSettingsCommandHandler(IApplicationDbContext context, IFileManager fileManager)
|
||||
{
|
||||
_context = context;
|
||||
_fileManager = fileManager;
|
||||
}
|
||||
|
||||
public async Task<SavePageSettingsResult> Handle(SavePageSettingsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.SitePageSettingsEntities
|
||||
.FirstOrDefaultAsync(x => x.PageKey == request.PageKey && !x.IsDeleted, cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
// ایجاد صفحه جدید (فقط برای seed اولیه — صفحات ثابت هستند)
|
||||
entity = new SitePageSettings
|
||||
{
|
||||
PageKey = request.PageKey,
|
||||
};
|
||||
_context.SitePageSettingsEntities.Add(entity);
|
||||
}
|
||||
|
||||
entity.Title = request.Title;
|
||||
entity.MetaDescription = request.MetaDescription;
|
||||
entity.HeroTitle = request.HeroTitle;
|
||||
entity.HeroSubtitle = request.HeroSubtitle;
|
||||
entity.IsActive = request.IsActive;
|
||||
entity.SettingsJson = request.SettingsJson;
|
||||
|
||||
// آپلود تصویر هیرو
|
||||
if (request.ImageFileBytes is { Length: > 0 })
|
||||
{
|
||||
var result = await _fileManager.UploadImageAsync(
|
||||
"Images/SitePageSettings",
|
||||
request.ImageFileBytes,
|
||||
request.ImageFileMime ?? "image/jpeg",
|
||||
request.ImageFileName,
|
||||
cancellationToken);
|
||||
|
||||
entity.HeroImagePath = result.Main.Path;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new SavePageSettingsResult { Id = entity.Id, Success = true };
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageSettings;
|
||||
|
||||
public class SavePageSettingsCommandValidator : AbstractValidator<SavePageSettingsCommand>
|
||||
{
|
||||
private static readonly string[] ValidPageKeys = { "landing", "about", "contact", "licenses" };
|
||||
|
||||
public SavePageSettingsCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.PageKey)
|
||||
.NotEmpty().WithMessage("کلید صفحه الزامی است")
|
||||
.MaximumLength(50)
|
||||
.Must(key => ValidPageKeys.Contains(key))
|
||||
.WithMessage("کلید صفحه باید یکی از مقادیر landing, about, contact, licenses باشد");
|
||||
|
||||
RuleFor(x => x.Title)
|
||||
.NotEmpty().WithMessage("عنوان صفحه الزامی است")
|
||||
.MaximumLength(200).WithMessage("عنوان صفحه حداکثر ۲۰۰ کاراکتر");
|
||||
|
||||
RuleFor(x => x.MetaDescription)
|
||||
.MaximumLength(300).WithMessage("توضیحات متا حداکثر ۳۰۰ کاراکتر");
|
||||
|
||||
RuleFor(x => x.HeroTitle)
|
||||
.MaximumLength(200).WithMessage("عنوان هیرو حداکثر ۲۰۰ کاراکتر");
|
||||
|
||||
RuleFor(x => x.HeroSubtitle)
|
||||
.MaximumLength(500).WithMessage("زیرعنوان هیرو حداکثر ۵۰۰ کاراکتر");
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.SitePageSettingsCQ.Queries.GetAllPageSettings;
|
||||
|
||||
// ── Query ──
|
||||
public class GetAllPageSettingsQuery : IRequest<List<PageSettingsSummaryDto>>
|
||||
{
|
||||
}
|
||||
|
||||
// ── Handler ──
|
||||
public class GetAllPageSettingsQueryHandler : IRequestHandler<GetAllPageSettingsQuery, List<PageSettingsSummaryDto>>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetAllPageSettingsQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<PageSettingsSummaryDto>> Handle(GetAllPageSettingsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _context.SitePageSettingsEntities
|
||||
.Where(x => !x.IsDeleted)
|
||||
.OrderBy(x => x.Id)
|
||||
.Select(x => new PageSettingsSummaryDto
|
||||
{
|
||||
Id = x.Id,
|
||||
PageKey = x.PageKey,
|
||||
Title = x.Title,
|
||||
IsActive = x.IsActive,
|
||||
ImageCount = x.Images.Count(i => !i.IsDeleted),
|
||||
LastModified = x.LastModified ?? x.Created
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
// ── DTO ──
|
||||
public class PageSettingsSummaryDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string PageKey { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public bool IsActive { get; set; }
|
||||
public int ImageCount { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Content;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.SitePageSettingsCQ.Queries.GetPageSettings;
|
||||
|
||||
// ── Query ──
|
||||
public class GetPageSettingsQuery : IRequest<PageSettingsDto?>
|
||||
{
|
||||
public string PageKey { get; set; } = default!;
|
||||
}
|
||||
|
||||
// ── Handler ──
|
||||
public class GetPageSettingsQueryHandler : IRequestHandler<GetPageSettingsQuery, PageSettingsDto?>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetPageSettingsQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<PageSettingsDto?> Handle(GetPageSettingsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.SitePageSettingsEntities
|
||||
.Include(x => x.Images.Where(i => !i.IsDeleted && i.IsActive).OrderBy(i => i.SortOrder))
|
||||
.FirstOrDefaultAsync(x => x.PageKey == request.PageKey && !x.IsDeleted, cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
return null;
|
||||
|
||||
return MapToDto(entity);
|
||||
}
|
||||
|
||||
public static PageSettingsDto MapToDto(SitePageSettings entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
PageKey = entity.PageKey,
|
||||
Title = entity.Title,
|
||||
MetaDescription = entity.MetaDescription,
|
||||
HeroTitle = entity.HeroTitle,
|
||||
HeroSubtitle = entity.HeroSubtitle,
|
||||
HeroImagePath = entity.HeroImagePath,
|
||||
IsActive = entity.IsActive,
|
||||
SettingsJson = entity.SettingsJson,
|
||||
Images = entity.Images.Select(i => new PageImageDto
|
||||
{
|
||||
Id = i.Id,
|
||||
ImageGroup = i.ImageGroup,
|
||||
Title = i.Title,
|
||||
Subtitle = i.Subtitle,
|
||||
Description = i.Description,
|
||||
ImagePath = i.ImagePath,
|
||||
ThumbnailPath = i.ThumbnailPath,
|
||||
LinkUrl = i.LinkUrl,
|
||||
IconName = i.IconName,
|
||||
SortOrder = i.SortOrder,
|
||||
IsActive = i.IsActive
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
// ── DTOs ──
|
||||
public class PageSettingsDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string PageKey { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? MetaDescription { get; set; }
|
||||
public string? HeroTitle { get; set; }
|
||||
public string? HeroSubtitle { get; set; }
|
||||
public string? HeroImagePath { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public string? SettingsJson { get; set; }
|
||||
public List<PageImageDto> Images { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PageImageDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string ImageGroup { get; set; } = string.Empty;
|
||||
public string? Title { get; set; }
|
||||
public string? Subtitle { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
public string? ThumbnailPath { get; set; }
|
||||
public string? LinkUrl { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
+18
@@ -1,6 +1,7 @@
|
||||
using CMSMicroservice.Application.Common.FileManager;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
||||
@@ -9,15 +10,18 @@ public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProducts
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IFileManager _fileManager;
|
||||
private readonly IInventoryService _inventoryService;
|
||||
private readonly ILogger<CreateNewProductsCommandHandler> _logger;
|
||||
|
||||
public CreateNewProductsCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IFileManager fileManager,
|
||||
IInventoryService inventoryService,
|
||||
ILogger<CreateNewProductsCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_fileManager = fileManager;
|
||||
_inventoryService = inventoryService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -70,6 +74,20 @@ public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProducts
|
||||
await _context.Products.AddAsync(entity, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// ایجاد رکورد موجودی در سیستم انبارداری
|
||||
try
|
||||
{
|
||||
await _inventoryService.InitializeInventoryAsync(
|
||||
entity.Id,
|
||||
ProductType.RegularProduct,
|
||||
0, // موجودی اولیه صفر — باید از طریق Inventory اضافه شود
|
||||
ct: cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to initialize inventory for Product Id={ProductId}", entity.Id);
|
||||
}
|
||||
|
||||
// Handle category assignments
|
||||
if (request.CategoryIds is { Count: > 0 })
|
||||
{
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using CMSMicroservice.Domain.Common;
|
||||
|
||||
namespace CMSMicroservice.Domain.Entities.Content;
|
||||
|
||||
/// <summary>
|
||||
/// تصاویر مرتبط با صفحات سایت
|
||||
/// شامل: مجوزها، اعضای تیم، ارزشها و سایر آیتمهای تصویری
|
||||
/// </summary>
|
||||
public class SitePageImage : BaseAuditableEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه صفحه مرتبط
|
||||
/// </summary>
|
||||
public long SitePageSettingsId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// گروه تصویر (licenses, team, values)
|
||||
/// </summary>
|
||||
public string ImageGroup { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// عنوان
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// زیرعنوان
|
||||
/// </summary>
|
||||
public string? Subtitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// توضیحات
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// مسیر تصویر اصلی
|
||||
/// </summary>
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// مسیر تامبنیل
|
||||
/// </summary>
|
||||
public string? ThumbnailPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// لینک خارجی (برای مجوزها: لینک به سایت مرجع)
|
||||
/// </summary>
|
||||
public string? LinkUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نام آیکون Material
|
||||
/// </summary>
|
||||
public string? IconName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ترتیب نمایش
|
||||
/// </summary>
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// فعال بودن
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// صفحه مرتبط
|
||||
/// </summary>
|
||||
public virtual SitePageSettings SitePageSettings { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using CMSMicroservice.Domain.Common;
|
||||
|
||||
namespace CMSMicroservice.Domain.Entities.Content;
|
||||
|
||||
/// <summary>
|
||||
/// تنظیمات صفحات سایت — هر صفحه یک رکورد با تنظیمات JSON تایپشده
|
||||
/// صفحات ثابت: landing, about, contact, licenses
|
||||
/// </summary>
|
||||
public class SitePageSettings : BaseAuditableEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// کلید یکتای صفحه (landing, about, contact, licenses)
|
||||
/// </summary>
|
||||
public string PageKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// عنوان صفحه
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// توضیح متا برای SEO
|
||||
/// </summary>
|
||||
public string? MetaDescription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// عنوان بخش Hero
|
||||
/// </summary>
|
||||
public string? HeroTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// زیرعنوان بخش Hero
|
||||
/// </summary>
|
||||
public string? HeroSubtitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// مسیر تصویر Hero
|
||||
/// </summary>
|
||||
public string? HeroImagePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// فعال بودن صفحه
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// تنظیمات اختصاصی صفحه به فرمت JSON
|
||||
/// ساختار JSON بر اساس PageKey متفاوت است
|
||||
/// </summary>
|
||||
public string? SettingsJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// تصاویر مرتبط با صفحه (مجوزها، تیم، ارزشها)
|
||||
/// </summary>
|
||||
public virtual ICollection<SitePageImage> Images { get; set; } = new List<SitePageImage>();
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.BackgroundServices;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس یکباره (Migration) برای مقداردهی اولیه انبارداری از محصولات قدیمی.
|
||||
/// محصولاتی که قبل از فعالسازی سیستم انبارداری ایجاد شدهاند رکورد InventoryItem ندارند.
|
||||
/// این Worker در استارتاپ اجرا شده، برای آنها رکورد میسازد و سپس متوقف میشود.
|
||||
/// توجه: محصولات جدید از طریق CreateProductCommandHandler خودکار رکورد انبار دریافت میکنند.
|
||||
/// این سرویس را میتوانید بعد از اجرای اولیه از DI حذف کنید.
|
||||
/// </summary>
|
||||
public class InventoryInitializerService : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<InventoryInitializerService> _logger;
|
||||
|
||||
public InventoryInitializerService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<InventoryInitializerService> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// صبر کوتاه برای اطمینان از آماده شدن دیتابیس
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
|
||||
|
||||
_logger.LogInformation("InventoryInitializerService started — scanning products for missing inventory records...");
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>();
|
||||
var inventoryService = scope.ServiceProvider.GetRequiredService<IInventoryService>();
|
||||
|
||||
var (regularCount, discountCount) = await InitializeAllProducts(context, inventoryService, stoppingToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"InventoryInitializerService completed — initialized {RegularCount} regular products and {DiscountCount} discount products",
|
||||
regularCount, discountCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "InventoryInitializerService encountered an error");
|
||||
}
|
||||
|
||||
// این Worker فقط یکبار اجرا میشود
|
||||
_logger.LogInformation("InventoryInitializerService finished — shutting down (one-time execution)");
|
||||
}
|
||||
|
||||
private async Task<(int regularCount, int discountCount)> InitializeAllProducts(
|
||||
IApplicationDbContext context,
|
||||
IInventoryService inventoryService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var regularCount = 0;
|
||||
var discountCount = 0;
|
||||
|
||||
// ── محصولات عادی ──
|
||||
// محصولاتی که هنوز InventoryItem برایشان ایجاد نشده
|
||||
var existingRegularProductIds = await context.InventoryItems
|
||||
.Where(i => !i.IsDeleted && i.ProductId != null)
|
||||
.Select(i => i.ProductId!.Value)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var regularProducts = await context.Products
|
||||
.Where(p => !p.IsDeleted && !existingRegularProductIds.Contains(p.Id))
|
||||
.Select(p => new { p.Id, p.Title, p.RemainingCount })
|
||||
.ToListAsync(ct);
|
||||
|
||||
_logger.LogInformation("Found {Count} regular products without inventory records", regularProducts.Count);
|
||||
|
||||
foreach (var product in regularProducts)
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
|
||||
try
|
||||
{
|
||||
var initialQty = Math.Max(0, product.RemainingCount);
|
||||
|
||||
await inventoryService.InitializeInventoryAsync(
|
||||
product.Id,
|
||||
ProductType.RegularProduct,
|
||||
initialQty,
|
||||
warehouseId: null, // انبار پیشفرض (ID=1)
|
||||
lowStockThreshold: 10,
|
||||
ct);
|
||||
|
||||
regularCount++;
|
||||
_logger.LogDebug(
|
||||
"Initialized inventory for Regular Product Id={ProductId} ({Title}), Qty={Qty}",
|
||||
product.Id, product.Title, initialQty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"Failed to initialize inventory for Regular Product Id={ProductId} ({Title})",
|
||||
product.Id, product.Title);
|
||||
}
|
||||
}
|
||||
|
||||
// ── محصولات تخفیفی ──
|
||||
var existingDiscountProductIds = await context.InventoryItems
|
||||
.Where(i => !i.IsDeleted && i.DiscountProductId != null)
|
||||
.Select(i => i.DiscountProductId!.Value)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var discountProducts = await context.DiscountProducts
|
||||
.Where(p => !p.IsDeleted && !existingDiscountProductIds.Contains(p.Id))
|
||||
.Select(p => new { p.Id, p.Title, p.RemainingCount })
|
||||
.ToListAsync(ct);
|
||||
|
||||
_logger.LogInformation("Found {Count} discount products without inventory records", discountProducts.Count);
|
||||
|
||||
foreach (var product in discountProducts)
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
|
||||
try
|
||||
{
|
||||
var initialQty = Math.Max(0, product.RemainingCount);
|
||||
|
||||
await inventoryService.InitializeInventoryAsync(
|
||||
product.Id,
|
||||
ProductType.DiscountProduct,
|
||||
initialQty,
|
||||
warehouseId: null,
|
||||
lowStockThreshold: 10,
|
||||
ct);
|
||||
|
||||
discountCount++;
|
||||
_logger.LogDebug(
|
||||
"Initialized inventory for Discount Product Id={ProductId} ({Title}), Qty={Qty}",
|
||||
product.Id, product.Title, initialQty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"Failed to initialize inventory for Discount Product Id={ProductId} ({Title})",
|
||||
product.Id, product.Title);
|
||||
}
|
||||
}
|
||||
|
||||
return (regularCount, discountCount);
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,9 @@ public static class ConfigureServices
|
||||
// Expire pending discount orders after 30 minutes
|
||||
services.AddHostedService<ExpirePendingOrdersService>();
|
||||
|
||||
// One-time: Initialize inventory records for existing products
|
||||
services.AddHostedService<InventoryInitializerService>();
|
||||
|
||||
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
|
||||
{
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
|
||||
@@ -152,4 +152,8 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
|
||||
// ============= Content Management DbSets =============
|
||||
public DbSet<SitePage> SitePages => Set<SitePage>();
|
||||
public DbSet<SitePageSection> SitePageSections => Set<SitePageSection>();
|
||||
|
||||
// ============= Site Page Settings (Simplified) =============
|
||||
public DbSet<SitePageSettings> SitePageSettingsEntities => Set<SitePageSettings>();
|
||||
public DbSet<SitePageImage> SitePageImages => Set<SitePageImage>();
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
using CMSMicroservice.Domain.Entities.Content;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class SitePageImageConfiguration : IEntityTypeConfiguration<SitePageImage>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SitePageImage> builder)
|
||||
{
|
||||
builder.ToTable("SitePageImages");
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.SitePageSettingsId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(x => x.ImageGroup)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50);
|
||||
|
||||
builder.Property(x => x.Title)
|
||||
.HasMaxLength(200);
|
||||
|
||||
builder.Property(x => x.Subtitle)
|
||||
.HasMaxLength(300);
|
||||
|
||||
builder.Property(x => x.ImagePath)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(x => x.IconName)
|
||||
.HasMaxLength(100);
|
||||
|
||||
builder.Property(x => x.LinkUrl)
|
||||
.HasMaxLength(500);
|
||||
|
||||
builder.Property(x => x.SortOrder)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0);
|
||||
|
||||
builder.Property(x => x.IsActive)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(true);
|
||||
|
||||
// رابطه با SitePageSettings
|
||||
builder.HasOne(x => x.SitePageSettings)
|
||||
.WithMany(x => x.Images)
|
||||
.HasForeignKey(x => x.SitePageSettingsId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// ایندکس ترکیبی
|
||||
builder.HasIndex(x => new { x.SitePageSettingsId, x.ImageGroup })
|
||||
.HasDatabaseName("IX_SitePageImages_SettingsId_Group");
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
using CMSMicroservice.Domain.Entities.Content;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class SitePageSettingsConfiguration : IEntityTypeConfiguration<SitePageSettings>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SitePageSettings> builder)
|
||||
{
|
||||
builder.ToTable("SitePageSettings");
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.PageKey)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50);
|
||||
|
||||
builder.Property(x => x.Title)
|
||||
.IsRequired()
|
||||
.HasMaxLength(200);
|
||||
|
||||
builder.Property(x => x.MetaDescription)
|
||||
.HasMaxLength(300);
|
||||
|
||||
builder.Property(x => x.HeroTitle)
|
||||
.HasMaxLength(200);
|
||||
|
||||
builder.Property(x => x.HeroSubtitle)
|
||||
.HasMaxLength(500);
|
||||
|
||||
builder.Property(x => x.IsActive)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(true);
|
||||
|
||||
// SettingsJson — ستون JSON بدون محدودیت طول
|
||||
builder.Property(x => x.SettingsJson)
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
// ایندکس یکتا روی PageKey
|
||||
builder.HasIndex(x => x.PageKey)
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_SitePageSettings_PageKey");
|
||||
}
|
||||
}
|
||||
+4674
File diff suppressed because it is too large
Load Diff
+102
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSitePageSettings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SitePageSettings",
|
||||
schema: "CMS",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
PageKey = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
MetaDescription = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: true),
|
||||
HeroTitle = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
HeroSubtitle = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
HeroImagePath = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
|
||||
SettingsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SitePageSettings", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SitePageImages",
|
||||
schema: "CMS",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
SitePageSettingsId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ImageGroup = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
Subtitle = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: true),
|
||||
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ImagePath = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
ThumbnailPath = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LinkUrl = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
IconName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
|
||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SitePageImages", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SitePageImages_SitePageSettings_SitePageSettingsId",
|
||||
column: x => x.SitePageSettingsId,
|
||||
principalSchema: "CMS",
|
||||
principalTable: "SitePageSettings",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SitePageImages_SettingsId_Group",
|
||||
schema: "CMS",
|
||||
table: "SitePageImages",
|
||||
columns: new[] { "SitePageSettingsId", "ImageGroup" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SitePageSettings_PageKey",
|
||||
schema: "CMS",
|
||||
table: "SitePageSettings",
|
||||
column: "PageKey",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SitePageImages",
|
||||
schema: "CMS");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SitePageSettings",
|
||||
schema: "CMS");
|
||||
}
|
||||
}
|
||||
}
|
||||
+156
@@ -826,6 +826,81 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("SitePages", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageImage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("IconName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("ImageGroup")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ImagePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("LinkUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<long>("SitePageSettingsId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<string>("Subtitle")
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("nvarchar(300)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SitePageSettingsId", "ImageGroup")
|
||||
.HasDatabaseName("IX_SitePageImages_SettingsId_Group");
|
||||
|
||||
b.ToTable("SitePageImages", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageSection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -900,6 +975,71 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("SitePageSections", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageSettings", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("HeroImagePath")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("HeroSubtitle")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("HeroTitle")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("MetaDescription")
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("nvarchar(300)");
|
||||
|
||||
b.Property<string>("PageKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("SettingsJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PageKey")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_SitePageSettings_PageKey");
|
||||
|
||||
b.ToTable("SitePageSettings", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Contract", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -3807,6 +3947,17 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("WeekDefinition");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageImage", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Content.SitePageSettings", "SitePageSettings")
|
||||
.WithMany("Images")
|
||||
.HasForeignKey("SitePageSettingsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("SitePageSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageSection", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Content.SitePage", "SitePage")
|
||||
@@ -4363,6 +4514,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("Sections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageSettings", b =>
|
||||
{
|
||||
b.Navigation("Images");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Contract", b =>
|
||||
{
|
||||
b.Navigation("UserContracts");
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>0.0.179</Version>
|
||||
<Version>0.0.180</Version>
|
||||
<DebugType>None</DebugType>
|
||||
<DebugSymbols>False</DebugSymbols>
|
||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||
@@ -71,6 +71,7 @@
|
||||
<Protobuf Include="Protos\blogcategory.proto" ProtoRoot="Protos\" GrpcServices="Both" />
|
||||
<Protobuf Include="Protos\blogpostimage.proto" ProtoRoot="Protos\" GrpcServices="Both" />
|
||||
<Protobuf Include="Protos\sitepage.proto" ProtoRoot="Protos\" GrpcServices="Both" />
|
||||
<Protobuf Include="Protos\sitepagesettings.proto" ProtoRoot="Protos\" GrpcServices="Both" />
|
||||
<!-- Image Resolver Service -->
|
||||
<Protobuf Include="Protos\imageresolver.proto" ProtoRoot="Protos\" GrpcServices="Both" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package sitepagesettings;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/api/annotations.proto";
|
||||
|
||||
option csharp_namespace = "CMSMicroservice.Protobuf.Protos.SitePageSettings";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// سرویس مدیریت صفحات سایت (نسخه سادهشده)
|
||||
// ۴ صفحه ثابت: landing, about, contact, licenses
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
service SitePageSettingsContract
|
||||
{
|
||||
// دریافت تنظیمات صفحه با کلید — مورد استفاده FrontOffice
|
||||
rpc GetPageSettings(GetPageSettingsRequest) returns (PageSettingsResponse){
|
||||
option (google.api.http) = { get: "/api/page-settings/{page_key}" };
|
||||
};
|
||||
|
||||
// دریافت لیست همه صفحات — مورد استفاده BackOffice
|
||||
rpc GetAllPageSettings(GetAllPageSettingsRequest) returns (GetAllPageSettingsResponse){
|
||||
option (google.api.http) = { get: "/api/page-settings" };
|
||||
};
|
||||
|
||||
// ذخیره تنظیمات صفحه — مورد استفاده BackOffice
|
||||
rpc SavePageSettings(SavePageSettingsRequest) returns (SavePageSettingsResponse){
|
||||
option (google.api.http) = { put: "/api/page-settings" body: "*" };
|
||||
};
|
||||
|
||||
// ذخیره تصویر صفحه (مجوزها، تیم، ارزشها) — مورد استفاده BackOffice
|
||||
rpc SavePageImage(SavePageImageRequest) returns (SavePageImageResponse){
|
||||
option (google.api.http) = { put: "/api/page-settings/images" body: "*" };
|
||||
};
|
||||
|
||||
// حذف تصویر صفحه — مورد استفاده BackOffice
|
||||
rpc DeletePageImage(DeletePageImageRequest) returns (google.protobuf.Empty){
|
||||
option (google.api.http) = { delete: "/api/page-settings/images/{id}" };
|
||||
};
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Get Page Settings
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
message GetPageSettingsRequest
|
||||
{
|
||||
string page_key = 1; // "landing" | "about" | "contact" | "licenses"
|
||||
}
|
||||
|
||||
message PageSettingsResponse
|
||||
{
|
||||
int64 id = 1;
|
||||
string page_key = 2;
|
||||
string title = 3;
|
||||
google.protobuf.StringValue meta_description = 4;
|
||||
google.protobuf.StringValue hero_title = 5;
|
||||
google.protobuf.StringValue hero_subtitle = 6;
|
||||
google.protobuf.StringValue hero_image_path = 7;
|
||||
bool is_active = 8;
|
||||
string settings_json = 9; // JSON تنظیمات — ساختار بر اساس page_key متفاوت
|
||||
repeated PageImageItem images = 10; // تصاویر مرتبط (مجوزها، تیم، ارزشها)
|
||||
}
|
||||
|
||||
message PageImageItem
|
||||
{
|
||||
int64 id = 1;
|
||||
string image_group = 2; // "licenses" | "team" | "values"
|
||||
google.protobuf.StringValue title = 3;
|
||||
google.protobuf.StringValue subtitle = 4;
|
||||
google.protobuf.StringValue description = 5;
|
||||
string image_path = 6;
|
||||
google.protobuf.StringValue thumbnail_path = 7;
|
||||
google.protobuf.StringValue link_url = 8;
|
||||
google.protobuf.StringValue icon_name = 9;
|
||||
int32 sort_order = 10;
|
||||
bool is_active = 11;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Get All Page Settings (Summary)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
message GetAllPageSettingsRequest {}
|
||||
|
||||
message GetAllPageSettingsResponse
|
||||
{
|
||||
repeated PageSettingsSummary pages = 1;
|
||||
}
|
||||
|
||||
message PageSettingsSummary
|
||||
{
|
||||
int64 id = 1;
|
||||
string page_key = 2;
|
||||
string title = 3;
|
||||
bool is_active = 4;
|
||||
int32 image_count = 5;
|
||||
google.protobuf.Timestamp last_modified = 6;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Save Page Settings
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
message SavePageSettingsRequest
|
||||
{
|
||||
string page_key = 1; // کلید صفحه (نمیتونه تغییر کنه)
|
||||
string title = 2;
|
||||
google.protobuf.StringValue meta_description = 3;
|
||||
google.protobuf.StringValue hero_title = 4;
|
||||
google.protobuf.StringValue hero_subtitle = 5;
|
||||
bool is_active = 6;
|
||||
string settings_json = 7; // JSON تنظیمات
|
||||
PageSettingsImageFile hero_image_file = 8; // آپلود تصویر Hero (اختیاری)
|
||||
}
|
||||
|
||||
message SavePageSettingsResponse
|
||||
{
|
||||
int64 id = 1;
|
||||
bool success = 2;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Save Page Image (Add/Update)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
message SavePageImageRequest
|
||||
{
|
||||
int64 id = 1; // 0 = ایجاد جدید، > 0 = ویرایش
|
||||
int64 site_page_settings_id = 2; // شناسه صفحه
|
||||
string image_group = 3; // "licenses" | "team" | "values"
|
||||
google.protobuf.StringValue title = 4;
|
||||
google.protobuf.StringValue subtitle = 5;
|
||||
google.protobuf.StringValue description = 6;
|
||||
google.protobuf.StringValue link_url = 7;
|
||||
google.protobuf.StringValue icon_name = 8;
|
||||
int32 sort_order = 9;
|
||||
bool is_active = 10;
|
||||
PageSettingsImageFile image_file = 11; // فایل تصویر (اختیاری برای ویرایش)
|
||||
}
|
||||
|
||||
message SavePageImageResponse
|
||||
{
|
||||
int64 id = 1;
|
||||
bool success = 2;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Delete Page Image
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
message DeletePageImageRequest
|
||||
{
|
||||
int64 id = 1;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// File Upload Model
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
message PageSettingsImageFile
|
||||
{
|
||||
bytes file = 1;
|
||||
string mime = 2;
|
||||
string file_name = 3;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using CMSMicroservice.Protobuf.Protos.SitePageSettings;
|
||||
using CMSMicroservice.Application.SitePageSettingsCQ.Commands.DeletePageImage;
|
||||
using CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageImage;
|
||||
using CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageSettings;
|
||||
using CMSMicroservice.Application.SitePageSettingsCQ.Queries.GetAllPageSettings;
|
||||
using CMSMicroservice.Application.SitePageSettingsCQ.Queries.GetPageSettings;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
|
||||
public class SitePageSettingsService : SitePageSettingsContract.SitePageSettingsContractBase
|
||||
{
|
||||
private readonly ISender _sender;
|
||||
|
||||
public SitePageSettingsService(ISender sender)
|
||||
{
|
||||
_sender = sender;
|
||||
}
|
||||
|
||||
// ── GetPageSettings ──
|
||||
public override async Task<PageSettingsResponse> GetPageSettings(GetPageSettingsRequest request, ServerCallContext context)
|
||||
{
|
||||
var query = new GetPageSettingsQuery { PageKey = request.PageKey };
|
||||
var result = await _sender.Send(query, context.CancellationToken);
|
||||
|
||||
if (result == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, $"صفحه با کلید '{request.PageKey}' یافت نشد"));
|
||||
|
||||
return MapToResponse(result);
|
||||
}
|
||||
|
||||
// ── GetAllPageSettings ──
|
||||
public override async Task<GetAllPageSettingsResponse> GetAllPageSettings(GetAllPageSettingsRequest request, ServerCallContext context)
|
||||
{
|
||||
var query = new GetAllPageSettingsQuery();
|
||||
var result = await _sender.Send(query, context.CancellationToken);
|
||||
|
||||
var response = new GetAllPageSettingsResponse();
|
||||
foreach (var item in result)
|
||||
{
|
||||
response.Pages.Add(new PageSettingsSummary
|
||||
{
|
||||
Id = item.Id,
|
||||
PageKey = item.PageKey,
|
||||
Title = item.Title,
|
||||
IsActive = item.IsActive,
|
||||
ImageCount = item.ImageCount,
|
||||
LastModified = Timestamp.FromDateTime(DateTime.SpecifyKind(item.LastModified, DateTimeKind.Utc))
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// ── SavePageSettings ──
|
||||
public override async Task<SavePageSettingsResponse> SavePageSettings(SavePageSettingsRequest request, ServerCallContext context)
|
||||
{
|
||||
var command = new SavePageSettingsCommand
|
||||
{
|
||||
PageKey = request.PageKey,
|
||||
Title = request.Title,
|
||||
MetaDescription = request.MetaDescription,
|
||||
HeroTitle = request.HeroTitle,
|
||||
HeroSubtitle = request.HeroSubtitle,
|
||||
IsActive = request.IsActive,
|
||||
SettingsJson = request.SettingsJson,
|
||||
ImageFileBytes = request.HeroImageFile?.File?.ToByteArray(),
|
||||
ImageFileMime = request.HeroImageFile?.Mime,
|
||||
ImageFileName = request.HeroImageFile?.FileName
|
||||
};
|
||||
|
||||
var result = await _sender.Send(command, context.CancellationToken);
|
||||
return new SavePageSettingsResponse { Id = result.Id, Success = result.Success };
|
||||
}
|
||||
|
||||
// ── SavePageImage ──
|
||||
public override async Task<SavePageImageResponse> SavePageImage(SavePageImageRequest request, ServerCallContext context)
|
||||
{
|
||||
var command = new SavePageImageCommand
|
||||
{
|
||||
Id = request.Id,
|
||||
SitePageSettingsId = request.SitePageSettingsId,
|
||||
ImageGroup = request.ImageGroup,
|
||||
Title = request.Title,
|
||||
Subtitle = request.Subtitle,
|
||||
Description = request.Description,
|
||||
LinkUrl = request.LinkUrl,
|
||||
IconName = request.IconName,
|
||||
SortOrder = request.SortOrder,
|
||||
IsActive = request.IsActive,
|
||||
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
|
||||
ImageFileMime = request.ImageFile?.Mime,
|
||||
ImageFileName = request.ImageFile?.FileName
|
||||
};
|
||||
|
||||
var result = await _sender.Send(command, context.CancellationToken);
|
||||
return new SavePageImageResponse { Id = result.Id, Success = result.Success };
|
||||
}
|
||||
|
||||
// ── DeletePageImage ──
|
||||
public override async Task<Empty> DeletePageImage(DeletePageImageRequest request, ServerCallContext context)
|
||||
{
|
||||
var command = new DeletePageImageCommand { Id = request.Id };
|
||||
await _sender.Send(command, context.CancellationToken);
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
// ── Private Mapping Helpers ──
|
||||
private static PageSettingsResponse MapToResponse(PageSettingsDto dto)
|
||||
{
|
||||
var response = new PageSettingsResponse
|
||||
{
|
||||
Id = dto.Id,
|
||||
PageKey = dto.PageKey,
|
||||
Title = dto.Title,
|
||||
MetaDescription = dto.MetaDescription,
|
||||
HeroTitle = dto.HeroTitle,
|
||||
HeroSubtitle = dto.HeroSubtitle,
|
||||
HeroImagePath = dto.HeroImagePath,
|
||||
IsActive = dto.IsActive,
|
||||
SettingsJson = dto.SettingsJson ?? string.Empty
|
||||
};
|
||||
|
||||
foreach (var img in dto.Images)
|
||||
{
|
||||
response.Images.Add(new PageImageItem
|
||||
{
|
||||
Id = img.Id,
|
||||
ImageGroup = img.ImageGroup,
|
||||
Title = img.Title,
|
||||
Subtitle = img.Subtitle,
|
||||
Description = img.Description,
|
||||
ImagePath = img.ImagePath,
|
||||
ThumbnailPath = img.ThumbnailPath,
|
||||
LinkUrl = img.LinkUrl,
|
||||
IconName = img.IconName,
|
||||
SortOrder = img.SortOrder,
|
||||
IsActive = img.IsActive
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,11 @@
|
||||
"JwtIssuer": "https://localhost",
|
||||
"JwtAudience": "https://localhost",
|
||||
"JwtExpiryInDays": 5,
|
||||
"Kestrel": {
|
||||
"EndpointDefaults": {
|
||||
"Protocols": "Http1AndHttp2"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Data Source=45.149.79.127,31433; Initial Catalog=KBS;User ID=sa;Password=YourStrong@Passw0rd;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
|
||||
"providerName": "System.Data.SqlClient"
|
||||
@@ -71,11 +76,6 @@
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Kestrel": {
|
||||
"EndpointDefaults": {
|
||||
"Protocols": "Http2"
|
||||
}
|
||||
},
|
||||
"Authentication": {
|
||||
"Authority": "https://ids.domain.com/",
|
||||
"Audience": "domain_api"
|
||||
|
||||
Reference in New Issue
Block a user