feat: integrate PYMS payment gateway, add blog/sitepage/image services, local file manager
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m44s
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m44s
Payment Gateway: - Add PYMSPaymentService: IPaymentGatewayService via gRPC to PYMS microservice - Add ZarinPalPaymentService: direct ZarinPal integration (backup) - Register 'pyms' payment provider in DI ConfigureServices - Add PYMS proto files (pyms_transaction.proto, pyms_public_messages.proto) - Fix VerifyDiscountWalletCharge: pass 'OK' as status instead of Authority - Update appsettings: PaymentProvider=pyms, sandbox mode, merchant ID Blog System: - Add BlogCategory, BlogPost, BlogPostImage entities and CQRS - Add proto files and gRPC services for blog management - Add Mapster profiles for blog responses Content Management: - Add SitePage entity and CQRS for static pages - Add proto and gRPC service for site pages Image/File Management: - Add LocalFileManager with disk storage + base64 serving + FMS fallback - Add ImagePathResolverInterceptor for gRPC responses - Add ImageResolverService for explicit image resolution - Add UploadsController for public file serving with FMS fallback - Add PaymentCallbackController for discount order payment callbacks Database: - Add blog and content entity migrations - Remove ImagePath MaxLength constraints - Remove old FileManagementService (replaced by LocalFileManager)
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.CreateBlogCategory;
|
||||
|
||||
public class CreateBlogCategoryCommand : IRequest<long>
|
||||
{
|
||||
public string Title { get; set; } = default!;
|
||||
public string Slug { get; set; } = default!;
|
||||
public string? Description { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.CreateBlogCategory;
|
||||
|
||||
public class CreateBlogCategoryCommandHandler : IRequestHandler<CreateBlogCategoryCommand, long>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public CreateBlogCategoryCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<long> Handle(CreateBlogCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = new BlogCategory
|
||||
{
|
||||
Title = request.Title,
|
||||
Slug = request.Slug.ToLower(),
|
||||
Description = request.Description,
|
||||
IconName = request.IconName,
|
||||
SortOrder = request.SortOrder,
|
||||
IsActive = request.IsActive
|
||||
};
|
||||
|
||||
_context.BlogCategories.Add(entity);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.CreateBlogCategory;
|
||||
|
||||
public class CreateBlogCategoryCommandValidator : AbstractValidator<CreateBlogCategoryCommand>
|
||||
{
|
||||
public CreateBlogCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Title)
|
||||
.NotEmpty().WithMessage("عنوان دستهبندی الزامی است")
|
||||
.MaximumLength(200).WithMessage("عنوان دستهبندی حداکثر ۲۰۰ کاراکتر");
|
||||
|
||||
RuleFor(x => x.Slug)
|
||||
.NotEmpty().WithMessage("اسلاگ الزامی است")
|
||||
.MaximumLength(200).WithMessage("اسلاگ حداکثر ۲۰۰ کاراکتر")
|
||||
.Matches(@"^[a-z0-9\-]+$").WithMessage("اسلاگ فقط شامل حروف کوچک، اعداد و خط تیره");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(1000).WithMessage("توضیحات حداکثر ۱۰۰۰ کاراکتر");
|
||||
|
||||
RuleFor(x => x.IconName)
|
||||
.MaximumLength(100).WithMessage("نام آیکون حداکثر ۱۰۰ کاراکتر");
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.DeleteBlogCategory;
|
||||
|
||||
public class DeleteBlogCategoryCommand : IRequest<Unit>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.DeleteBlogCategory;
|
||||
|
||||
public class DeleteBlogCategoryCommandHandler : IRequestHandler<DeleteBlogCategoryCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public DeleteBlogCategoryCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(DeleteBlogCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.BlogCategories.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
if (entity == null || entity.IsDeleted)
|
||||
throw new NotFoundException(nameof(BlogCategory), request.Id);
|
||||
|
||||
entity.IsDeleted = true;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.UpdateBlogCategory;
|
||||
|
||||
public class UpdateBlogCategoryCommand : IRequest<Unit>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = default!;
|
||||
public string Slug { get; set; } = default!;
|
||||
public string? Description { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.UpdateBlogCategory;
|
||||
|
||||
public class UpdateBlogCategoryCommandHandler : IRequestHandler<UpdateBlogCategoryCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public UpdateBlogCategoryCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(UpdateBlogCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.BlogCategories.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
if (entity == null || entity.IsDeleted)
|
||||
throw new NotFoundException(nameof(BlogCategory), request.Id);
|
||||
|
||||
entity.Title = request.Title;
|
||||
entity.Slug = request.Slug.ToLower();
|
||||
entity.Description = request.Description;
|
||||
entity.IconName = request.IconName;
|
||||
entity.SortOrder = request.SortOrder;
|
||||
entity.IsActive = request.IsActive;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.UpdateBlogCategory;
|
||||
|
||||
public class UpdateBlogCategoryCommandValidator : AbstractValidator<UpdateBlogCategoryCommand>
|
||||
{
|
||||
public UpdateBlogCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Id)
|
||||
.GreaterThan(0).WithMessage("شناسه دستهبندی نامعتبر است");
|
||||
|
||||
RuleFor(x => x.Title)
|
||||
.NotEmpty().WithMessage("عنوان دستهبندی الزامی است")
|
||||
.MaximumLength(200).WithMessage("عنوان دستهبندی حداکثر ۲۰۰ کاراکتر");
|
||||
|
||||
RuleFor(x => x.Slug)
|
||||
.NotEmpty().WithMessage("اسلاگ الزامی است")
|
||||
.MaximumLength(200).WithMessage("اسلاگ حداکثر ۲۰۰ کاراکتر")
|
||||
.Matches(@"^[a-z0-9\-]+$").WithMessage("اسلاگ فقط شامل حروف کوچک، اعداد و خط تیره");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(1000).WithMessage("توضیحات حداکثر ۱۰۰۰ کاراکتر");
|
||||
|
||||
RuleFor(x => x.IconName)
|
||||
.MaximumLength(100).WithMessage("نام آیکون حداکثر ۱۰۰ کاراکتر");
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
using CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetActiveBlogCategories;
|
||||
|
||||
public class GetActiveBlogCategoriesQuery : IRequest<List<BlogCategoryDto>>
|
||||
{
|
||||
}
|
||||
|
||||
public class GetActiveBlogCategoriesQueryHandler : IRequestHandler<GetActiveBlogCategoriesQuery, List<BlogCategoryDto>>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetActiveBlogCategoriesQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<BlogCategoryDto>> Handle(GetActiveBlogCategoriesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var categories = await _context.BlogCategories
|
||||
.Include(x => x.BlogPostCategories)
|
||||
.Where(x => !x.IsDeleted && x.IsActive)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.ThenBy(x => x.Title)
|
||||
.Select(x => new BlogCategoryDto
|
||||
{
|
||||
Id = x.Id,
|
||||
Title = x.Title,
|
||||
Slug = x.Slug,
|
||||
Description = x.Description,
|
||||
IconName = x.IconName,
|
||||
SortOrder = x.SortOrder,
|
||||
IsActive = x.IsActive,
|
||||
PostCount = x.BlogPostCategories.Count,
|
||||
Created = x.Created,
|
||||
LastModified = x.LastModified
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return categories;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetAllBlogCategories;
|
||||
|
||||
public class GetAllBlogCategoriesQuery : IRequest<GetAllBlogCategoriesResponseDto>
|
||||
{
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
public string? SearchTerm { get; set; }
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
using CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetAllBlogCategories;
|
||||
|
||||
public class GetAllBlogCategoriesQueryHandler : IRequestHandler<GetAllBlogCategoriesQuery, GetAllBlogCategoriesResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetAllBlogCategoriesQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetAllBlogCategoriesResponseDto> Handle(GetAllBlogCategoriesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.BlogCategories
|
||||
.Include(x => x.BlogPostCategories)
|
||||
.Where(x => !x.IsDeleted);
|
||||
|
||||
if (!string.IsNullOrEmpty(request.SearchTerm))
|
||||
{
|
||||
var term = request.SearchTerm.ToLower();
|
||||
query = query.Where(x => x.Title.ToLower().Contains(term) || x.Slug.ToLower().Contains(term));
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
var categories = await query
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.ThenBy(x => x.Title)
|
||||
.Skip((request.PageNumber - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.Select(x => new BlogCategoryDto
|
||||
{
|
||||
Id = x.Id,
|
||||
Title = x.Title,
|
||||
Slug = x.Slug,
|
||||
Description = x.Description,
|
||||
IconName = x.IconName,
|
||||
SortOrder = x.SortOrder,
|
||||
IsActive = x.IsActive,
|
||||
PostCount = x.BlogPostCategories.Count,
|
||||
Created = x.Created,
|
||||
LastModified = x.LastModified
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var metaData = new MetaData
|
||||
{
|
||||
TotalCount = totalCount,
|
||||
PageSize = request.PageSize,
|
||||
CurrentPage = request.PageNumber,
|
||||
TotalPage = (int)Math.Ceiling(totalCount / (double)request.PageSize),
|
||||
HasNext = request.PageNumber < (int)Math.Ceiling(totalCount / (double)request.PageSize),
|
||||
HasPrevious = request.PageNumber > 1
|
||||
};
|
||||
|
||||
return new GetAllBlogCategoriesResponseDto { MetaData = metaData, Models = categories };
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
using CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetAllBlogCategories;
|
||||
|
||||
public class GetAllBlogCategoriesResponseDto
|
||||
{
|
||||
public MetaData MetaData { get; set; } = default!;
|
||||
public List<BlogCategoryDto> Models { get; set; } = new();
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
|
||||
|
||||
public class BlogCategoryDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = default!;
|
||||
public string Slug { get; set; } = default!;
|
||||
public string? Description { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public int PostCount { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime? LastModified { get; set; }
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
|
||||
|
||||
public class GetBlogCategoryQuery : IRequest<BlogCategoryDto>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
|
||||
|
||||
public class GetBlogCategoryQueryHandler : IRequestHandler<GetBlogCategoryQuery, BlogCategoryDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetBlogCategoryQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<BlogCategoryDto> Handle(GetBlogCategoryQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.BlogCategories
|
||||
.Include(x => x.BlogPostCategories)
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
throw new NotFoundException(nameof(BlogCategory), request.Id);
|
||||
|
||||
return new BlogCategoryDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
Title = entity.Title,
|
||||
Slug = entity.Slug,
|
||||
Description = entity.Description,
|
||||
IconName = entity.IconName,
|
||||
SortOrder = entity.SortOrder,
|
||||
IsActive = entity.IsActive,
|
||||
PostCount = entity.BlogPostCategories.Count,
|
||||
Created = entity.Created,
|
||||
LastModified = entity.LastModified
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user