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:
@@ -0,0 +1,231 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CMSMicroservice.Protobuf.Protos.BlogPost;
|
||||
using CMSMicroservice.WebApi.Common.Services;
|
||||
using CMSMicroservice.Application.BlogPostCQ.Commands.CreateBlogPost;
|
||||
using CMSMicroservice.Application.BlogPostCQ.Commands.UpdateBlogPost;
|
||||
using CMSMicroservice.Application.BlogPostCQ.Commands.DeleteBlogPost;
|
||||
using CMSMicroservice.Application.BlogPostCQ.Commands.PublishBlogPost;
|
||||
using CMSMicroservice.Application.BlogPostCQ.Commands.ArchiveBlogPost;
|
||||
using CMSMicroservice.Application.BlogPostCQ.Commands.IncrementViewCount;
|
||||
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPostBySlug;
|
||||
using CMSMicroservice.Application.BlogPostCQ.Queries.GetAllBlogPosts;
|
||||
using CMSMicroservice.Application.BlogPostCQ.Queries.GetPublishedBlogPosts;
|
||||
using CMSMicroservice.Application.BlogPostCQ.Queries.GetFeaturedBlogPosts;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
using Mapster;
|
||||
using MediatR;
|
||||
using AppModels = CMSMicroservice.Application.Common.Models;
|
||||
using ProtoMetaData = CMSMicroservice.Protobuf.Protos.MetaData;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
|
||||
public class BlogPostService : BlogPostContract.BlogPostContractBase
|
||||
{
|
||||
private readonly ISender _sender;
|
||||
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
|
||||
|
||||
public BlogPostService(ISender sender, IDispatchRequestToCQRS dispatchRequestToCQRS)
|
||||
{
|
||||
_sender = sender;
|
||||
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
||||
}
|
||||
|
||||
public override async Task<CreateBlogPostResponse> CreateBlogPost(CreateBlogPostRequest request, ServerCallContext context)
|
||||
{
|
||||
var command = new CreateBlogPostCommand
|
||||
{
|
||||
Title = request.Title,
|
||||
Slug = request.Slug,
|
||||
Summary = request.Summary,
|
||||
HtmlContent = request.HtmlContent,
|
||||
FeaturedImagePath = request.FeaturedImagePath,
|
||||
FeaturedImageThumbnailPath = request.FeaturedImageThumbnailPath,
|
||||
CategoryIds = request.CategoryIds.ToList(),
|
||||
TagIds = request.TagIds.ToList(),
|
||||
IsFeatured = request.IsFeatured,
|
||||
SortOrder = request.SortOrder,
|
||||
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
|
||||
ImageFileMime = request.ImageFile?.Mime,
|
||||
ImageFileName = request.ImageFile?.FileName
|
||||
};
|
||||
|
||||
var result = await _sender.Send(command, context.CancellationToken);
|
||||
return new CreateBlogPostResponse { Id = result };
|
||||
}
|
||||
|
||||
public override async Task<Empty> UpdateBlogPost(UpdateBlogPostRequest request, ServerCallContext context)
|
||||
{
|
||||
var command = new UpdateBlogPostCommand
|
||||
{
|
||||
Id = request.Id,
|
||||
Title = request.Title,
|
||||
Slug = request.Slug,
|
||||
Summary = request.Summary,
|
||||
HtmlContent = request.HtmlContent,
|
||||
FeaturedImagePath = request.FeaturedImagePath,
|
||||
FeaturedImageThumbnailPath = request.FeaturedImageThumbnailPath,
|
||||
CategoryIds = request.CategoryIds.ToList(),
|
||||
TagIds = request.TagIds.ToList(),
|
||||
IsFeatured = request.IsFeatured,
|
||||
SortOrder = request.SortOrder,
|
||||
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
|
||||
ImageFileMime = request.ImageFile?.Mime,
|
||||
ImageFileName = request.ImageFile?.FileName
|
||||
};
|
||||
|
||||
await _sender.Send(command, context.CancellationToken);
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
public override async Task<Empty> DeleteBlogPost(DeleteBlogPostRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<DeleteBlogPostRequest, DeleteBlogPostCommand>(request, context);
|
||||
}
|
||||
|
||||
public override async Task<GetBlogPostResponse> GetBlogPost(GetBlogPostRequest request, ServerCallContext context)
|
||||
{
|
||||
var query = new GetBlogPostQuery { Id = request.Id };
|
||||
var result = await _sender.Send(query, context.CancellationToken);
|
||||
return MapBlogPostDtoToResponse(result);
|
||||
}
|
||||
|
||||
public override async Task<GetBlogPostResponse> GetBlogPostBySlug(GetBlogPostBySlugRequest request, ServerCallContext context)
|
||||
{
|
||||
var query = new GetBlogPostBySlugQuery { Slug = request.Slug };
|
||||
var result = await _sender.Send(query, context.CancellationToken);
|
||||
return MapBlogPostDtoToResponse(result);
|
||||
}
|
||||
|
||||
public override async Task<GetAllBlogPostsResponse> GetAllBlogPosts(GetAllBlogPostsRequest request, ServerCallContext context)
|
||||
{
|
||||
var query = new GetAllBlogPostsQuery
|
||||
{
|
||||
PageNumber = request.PaginationState?.PageNumber ?? 1,
|
||||
PageSize = request.PaginationState?.PageSize ?? 10,
|
||||
SortBy = request.SortBy,
|
||||
SearchTerm = request.Filter?.SearchTerm,
|
||||
Status = request.Filter?.Status.HasValue == true ? (BlogPostStatus?)request.Filter.Status.Value : null,
|
||||
CategoryId = request.Filter?.CategoryId,
|
||||
IsFeatured = request.Filter?.IsFeatured
|
||||
};
|
||||
|
||||
var result = await _sender.Send(query, context.CancellationToken);
|
||||
return MapAllBlogPostsResponse(result);
|
||||
}
|
||||
|
||||
public override async Task<GetAllBlogPostsResponse> GetPublishedBlogPosts(GetPublishedBlogPostsRequest request, ServerCallContext context)
|
||||
{
|
||||
var query = new GetPublishedBlogPostsQuery
|
||||
{
|
||||
PageNumber = request.PaginationState?.PageNumber ?? 1,
|
||||
PageSize = request.PaginationState?.PageSize ?? 10,
|
||||
SearchTerm = request.SearchTerm,
|
||||
CategoryId = request.CategoryId
|
||||
};
|
||||
|
||||
var result = await _sender.Send(query, context.CancellationToken);
|
||||
return MapAllBlogPostsResponse(result);
|
||||
}
|
||||
|
||||
public override async Task<GetAllBlogPostsResponse> GetFeaturedBlogPosts(GetFeaturedBlogPostsRequest request, ServerCallContext context)
|
||||
{
|
||||
var query = new GetFeaturedBlogPostsQuery { Count = request.Count > 0 ? request.Count : 5 };
|
||||
var items = await _sender.Send(query, context.CancellationToken);
|
||||
|
||||
var response = new GetAllBlogPostsResponse();
|
||||
foreach (var item in items)
|
||||
response.Models.Add(MapToListItem(item));
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<PublishBlogPostResponse> PublishBlogPost(PublishBlogPostRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<PublishBlogPostRequest, PublishBlogPostCommand, PublishBlogPostResponse>(request, context);
|
||||
}
|
||||
|
||||
public override async Task<ArchiveBlogPostResponse> ArchiveBlogPost(ArchiveBlogPostRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<ArchiveBlogPostRequest, ArchiveBlogPostCommand, ArchiveBlogPostResponse>(request, context);
|
||||
}
|
||||
|
||||
public override async Task<Empty> IncrementViewCount(IncrementViewCountRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<IncrementViewCountRequest, IncrementViewCountCommand>(request, context);
|
||||
}
|
||||
|
||||
// ── Private Mapping Helpers ──
|
||||
|
||||
private static GetBlogPostResponse MapBlogPostDtoToResponse(BlogPostDto dto)
|
||||
{
|
||||
var response = new GetBlogPostResponse
|
||||
{
|
||||
Id = dto.Id,
|
||||
Title = dto.Title ?? string.Empty,
|
||||
Slug = dto.Slug ?? string.Empty,
|
||||
Summary = dto.Summary,
|
||||
HtmlContent = dto.HtmlContent ?? string.Empty,
|
||||
FeaturedImagePath = dto.FeaturedImagePath,
|
||||
FeaturedImageThumbnailPath = dto.FeaturedImageThumbnailPath,
|
||||
Status = (int)dto.Status,
|
||||
StatusName = dto.StatusName ?? string.Empty,
|
||||
ViewCount = dto.ViewCount,
|
||||
AuthorUserId = dto.AuthorUserId,
|
||||
IsFeatured = dto.IsFeatured,
|
||||
SortOrder = dto.SortOrder,
|
||||
Created = dto.Created != default ? Timestamp.FromDateTime(DateTime.SpecifyKind(dto.Created, DateTimeKind.Utc)) : null,
|
||||
LastModified = dto.LastModified.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(dto.LastModified.Value, DateTimeKind.Utc)) : null,
|
||||
PublishedAt = dto.PublishedAt.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(dto.PublishedAt.Value, DateTimeKind.Utc)) : null
|
||||
};
|
||||
|
||||
if (dto.Categories != null)
|
||||
foreach (var c in dto.Categories)
|
||||
response.Categories.Add(new BlogPostCategoryInfo { Id = c.Id, Title = c.Title ?? string.Empty, Slug = c.Slug ?? string.Empty });
|
||||
|
||||
if (dto.Tags != null)
|
||||
foreach (var t in dto.Tags)
|
||||
response.Tags.Add(new BlogPostTagInfo { Id = t.Id, Title = t.Title ?? string.Empty, Name = t.Name ?? string.Empty });
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static GetAllBlogPostsResponse MapAllBlogPostsResponse(GetAllBlogPostsResponseDto dto)
|
||||
{
|
||||
var response = new GetAllBlogPostsResponse
|
||||
{
|
||||
MetaData = dto.MetaData.Adapt<ProtoMetaData>()
|
||||
};
|
||||
|
||||
foreach (var item in dto.Models)
|
||||
response.Models.Add(MapToListItem(item));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static BlogPostListItem MapToListItem(BlogPostListItemDto item)
|
||||
{
|
||||
var listItem = new BlogPostListItem
|
||||
{
|
||||
Id = item.Id,
|
||||
Title = item.Title ?? string.Empty,
|
||||
Slug = item.Slug ?? string.Empty,
|
||||
Summary = item.Summary,
|
||||
FeaturedImageThumbnailPath = item.FeaturedImageThumbnailPath,
|
||||
Status = item.Status,
|
||||
StatusName = item.StatusName ?? string.Empty,
|
||||
ViewCount = item.ViewCount,
|
||||
IsFeatured = item.IsFeatured,
|
||||
Created = item.Created != default ? Timestamp.FromDateTime(DateTime.SpecifyKind(item.Created, DateTimeKind.Utc)) : null,
|
||||
PublishedAt = item.PublishedAt.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(item.PublishedAt.Value, DateTimeKind.Utc)) : null
|
||||
};
|
||||
|
||||
if (item.Categories != null)
|
||||
foreach (var c in item.Categories)
|
||||
listItem.Categories.Add(new BlogPostCategoryInfo { Id = c.Id, Title = c.Title ?? string.Empty, Slug = c.Slug ?? string.Empty });
|
||||
|
||||
return listItem;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user