Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 167991890c | |||
| ef1716e243 | |||
| c2ccd32f39 | |||
| c435319025 | |||
| 18a65de8c7 | |||
| e2e09ac4e8 | |||
| 61a0d68b7c | |||
| 1c4beb5f05 | |||
| 163a0a2f1a | |||
| e77611012b | |||
| a39a36e66d | |||
| 207f53ef80 | |||
| 2502cbbda2 | |||
| 5a4e4a960d | |||
| dc1e00f514 | |||
| 9d929dc7fb | |||
| 9b7d61593b | |||
| e3e3e8cec4 | |||
| 2885ff20c0 | |||
| f2a0b4d211 | |||
| 6c9f3dd671 | |||
| d1ca72300d | |||
| 1a425f0d93 | |||
| 6f8aefc2ce | |||
| 2bd5c7db78 | |||
| 0728ec2b76 | |||
| 1425fb187b | |||
| 81d2b39ee1 | |||
| fce194a195 |
@@ -26,7 +26,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
DAEMON
|
DAEMON
|
||||||
echo "🚀 Starting Docker daemon..."
|
echo "🚀 Starting Docker daemon..."
|
||||||
dockerd &
|
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
|
||||||
|
|
||||||
# Wait up to 3 minutes for Docker to be ready
|
# Wait up to 3 minutes for Docker to be ready
|
||||||
for i in $(seq 1 90); do
|
for i in $(seq 1 90); do
|
||||||
@@ -50,14 +50,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/CMS.git .
|
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/CMS.git .
|
||||||
|
|
||||||
|
- name: Login to Docker registries
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||||
|
|
||||||
- name: Publish Protobuf packages
|
- name: Publish Protobuf packages
|
||||||
run: |
|
run: |
|
||||||
echo "📦 Publishing Protobuf packages..."
|
echo "📦 Publishing Protobuf packages..."
|
||||||
docker run --rm -v $(pwd):/src -w /src \
|
docker run --rm --network host -v $(pwd):/src -w /src \
|
||||||
194.5.195.53:32082/dotnet/sdk:9.0 sh -c '
|
194.5.195.53:32082/dotnet/sdk:9.0 sh -c '
|
||||||
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
|
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
|
||||||
echo "📦 $proj"
|
echo "📦 $proj"
|
||||||
dotnet restore "$proj"
|
dotnet restore "$proj" --configfile src/NuGet.config
|
||||||
dotnet build "$proj" -c Release --no-restore
|
dotnet build "$proj" -c Release --no-restore
|
||||||
dotnet pack "$proj" -c Release --no-build -o "$(dirname $proj)/nupkg"
|
dotnet pack "$proj" -c Release --no-build -o "$(dirname $proj)/nupkg"
|
||||||
for nupkg in $(dirname $proj)/nupkg/*.nupkg; do
|
for nupkg in $(dirname $proj)/nupkg/*.nupkg; do
|
||||||
@@ -72,11 +77,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
run: |
|
run: |
|
||||||
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest .
|
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest .
|
||||||
|
|
||||||
- name: Push to Registry
|
- name: Push to Registry
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
|
||||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||||
|
|
||||||
- name: Deploy to Kubernetes
|
- name: Deploy to Kubernetes
|
||||||
|
|||||||
@@ -8,65 +8,70 @@ on:
|
|||||||
env:
|
env:
|
||||||
REGISTRY: 194.5.195.53:30080
|
REGISTRY: 194.5.195.53:30080
|
||||||
IMAGE_NAME: admin/cms
|
IMAGE_NAME: admin/cms
|
||||||
|
K8S_SERVER: 45.149.79.127
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: docker:latest
|
image: 194.5.195.53:32082/docker-sshpass:latest
|
||||||
options: --privileged
|
options: --privileged
|
||||||
env:
|
|
||||||
HTTP_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
|
|
||||||
HTTPS_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
|
|
||||||
NO_PROXY: localhost,127.0.0.1,gitea-svc,45.149.79.127,10.0.0.0/8
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies
|
- name: Start Docker daemon
|
||||||
run: |
|
|
||||||
apk add --no-cache git curl
|
|
||||||
|
|
||||||
# Install kubectl with fixed version
|
|
||||||
KUBECTL_VERSION="v1.31.0"
|
|
||||||
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
|
|
||||||
chmod +x kubectl
|
|
||||||
mv kubectl /usr/local/bin/
|
|
||||||
|
|
||||||
- name: Start Docker daemon with insecure registry
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /etc/docker
|
mkdir -p /etc/docker
|
||||||
cat > /etc/docker/daemon.json << 'DAEMON'
|
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||||
{
|
{
|
||||||
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32082", "gitea-svc:3000"]
|
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32082"]
|
||||||
}
|
}
|
||||||
DAEMON
|
DAEMON
|
||||||
mkdir -p ~/.docker
|
echo "🚀 Starting Docker daemon..."
|
||||||
cat > ~/.docker/config.json << 'CONF'
|
|
||||||
{
|
|
||||||
"proxies": {
|
|
||||||
"default": {
|
|
||||||
"httpProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
|
|
||||||
"httpsProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
|
|
||||||
"noProxy": "localhost,127.0.0.1,gitea-svc,45.149.79.127,10.0.0.0/8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
dockerd &
|
dockerd &
|
||||||
for i in $(seq 1 30); do
|
|
||||||
docker info >/dev/null 2>&1 && break || sleep 2
|
for i in $(seq 1 90); do
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
echo "✅ Docker daemon is ready (attempt $i)"
|
||||||
|
docker version
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
docker info
|
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
echo "❌ Docker daemon failed to start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch production http://gitea-svc:3000/admin/CMS.git .
|
git clone --depth 1 --branch production http://gitea-svc:3000/admin/CMS.git .
|
||||||
git log -1 --format="%H %s"
|
|
||||||
|
- name: Publish Protobuf packages
|
||||||
|
run: |
|
||||||
|
echo "📦 Publishing Protobuf packages..."
|
||||||
|
docker run --rm -v $(pwd):/src -w /src \
|
||||||
|
194.5.195.53:32082/dotnet/sdk:9.0 sh -c '
|
||||||
|
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
|
||||||
|
echo "📦 $proj"
|
||||||
|
dotnet restore "$proj"
|
||||||
|
dotnet build "$proj" -c Release --no-restore
|
||||||
|
dotnet pack "$proj" -c Release --no-build -o "$(dirname $proj)/nupkg"
|
||||||
|
for nupkg in $(dirname $proj)/nupkg/*.nupkg; do
|
||||||
|
[ -f "$nupkg" ] && dotnet nuget push "$nupkg" \
|
||||||
|
--source "http://194.5.195.53:32081/repository/foursat-nuget-hosted/index.json" \
|
||||||
|
--api-key "admin:87zH26nbqT" \
|
||||||
|
--skip-duplicate --allow-insecure-connections || true
|
||||||
|
done
|
||||||
|
done
|
||||||
|
'
|
||||||
|
echo "✅ Protobuf packages done!"
|
||||||
|
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
run: |
|
run: |
|
||||||
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
||||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
|
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
|
||||||
--build-arg HTTP_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
|
|
||||||
--build-arg HTTPS_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
|
|
||||||
.
|
.
|
||||||
|
|
||||||
- name: Push to Registry
|
- name: Push to Registry
|
||||||
@@ -77,12 +82,5 @@ jobs:
|
|||||||
|
|
||||||
- name: Deploy to Production
|
- name: Deploy to Production
|
||||||
run: |
|
run: |
|
||||||
# Setup kubeconfig for PRODUCTION
|
sshpass -p "${{ secrets.K8S_SSH_PASSWORD }}" ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} \
|
||||||
mkdir -p ~/.kube
|
"kubectl rollout restart deployment/cms && kubectl rollout status deployment/cms --timeout=5m" || echo "Deployment pending"
|
||||||
echo "${{ secrets.KUBECONFIG_PROD }}" | base64 -d > ~/.kube/config
|
|
||||||
|
|
||||||
# Restart deployment to pull new image
|
|
||||||
kubectl rollout restart deployment/cms || echo "Deployment doesn't exist yet"
|
|
||||||
|
|
||||||
# Wait for rollout to complete
|
|
||||||
kubectl rollout status deployment/cms --timeout=5m || echo "Deployment rollout pending"
|
|
||||||
|
|||||||
+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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.ArchiveBlogPost;
|
||||||
|
|
||||||
|
public class ArchiveBlogPostCommand : IRequest<ArchiveBlogPostResult>
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ArchiveBlogPostResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.ArchiveBlogPost;
|
||||||
|
|
||||||
|
public class ArchiveBlogPostCommandHandler : IRequestHandler<ArchiveBlogPostCommand, ArchiveBlogPostResult>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ILogger<ArchiveBlogPostCommandHandler> _logger;
|
||||||
|
|
||||||
|
public ArchiveBlogPostCommandHandler(IApplicationDbContext context, ILogger<ArchiveBlogPostCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ArchiveBlogPostResult> Handle(ArchiveBlogPostCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var post = await _context.BlogPosts.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
|
||||||
|
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
||||||
|
|
||||||
|
post.Status = BlogPostStatus.Archived;
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Blog post archived. Id: {Id}, Title: {Title}", post.Id, post.Title);
|
||||||
|
|
||||||
|
return new ArchiveBlogPostResult { Success = true, Message = "مقاله آرشیو شد" };
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.CreateBlogPost;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دستور ایجاد مقاله جدید
|
||||||
|
/// </summary>
|
||||||
|
public class CreateBlogPostCommand : IRequest<long>
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
public string HtmlContent { get; set; } = string.Empty;
|
||||||
|
public string? FeaturedImagePath { get; set; }
|
||||||
|
public string? FeaturedImageThumbnailPath { get; set; }
|
||||||
|
public List<long> CategoryIds { get; set; } = new();
|
||||||
|
public List<long> TagIds { get; set; } = new();
|
||||||
|
public bool IsFeatured { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
|
||||||
|
// Image upload properties
|
||||||
|
public byte[]? ImageFileBytes { get; set; }
|
||||||
|
public string? ImageFileMime { get; set; }
|
||||||
|
public string? ImageFileName { get; set; }
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities.Blog;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.CreateBlogPost;
|
||||||
|
|
||||||
|
public class CreateBlogPostCommandHandler : IRequestHandler<CreateBlogPostCommand, long>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
private readonly IFileManager _fileManager;
|
||||||
|
private readonly ILogger<CreateBlogPostCommandHandler> _logger;
|
||||||
|
|
||||||
|
public CreateBlogPostCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
ICurrentUserService currentUser,
|
||||||
|
IFileManager fileManager,
|
||||||
|
ILogger<CreateBlogPostCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
_fileManager = fileManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> Handle(CreateBlogPostCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var currentUserId = _currentUser.UserId;
|
||||||
|
if (string.IsNullOrEmpty(currentUserId) || !long.TryParse(currentUserId, out var authorUserId))
|
||||||
|
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
|
||||||
|
|
||||||
|
var post = new BlogPost
|
||||||
|
{
|
||||||
|
Title = request.Title.Trim(),
|
||||||
|
Slug = string.IsNullOrWhiteSpace(request.Slug)
|
||||||
|
? $"post-{Guid.NewGuid():N}".Substring(0, 20)
|
||||||
|
: request.Slug.Trim().ToLower(),
|
||||||
|
Summary = request.Summary?.Trim(),
|
||||||
|
HtmlContent = request.HtmlContent,
|
||||||
|
FeaturedImagePath = request.FeaturedImagePath,
|
||||||
|
FeaturedImageThumbnailPath = request.FeaturedImageThumbnailPath,
|
||||||
|
Status = BlogPostStatus.Draft,
|
||||||
|
AuthorUserId = authorUserId,
|
||||||
|
IsFeatured = request.IsFeatured,
|
||||||
|
SortOrder = request.SortOrder,
|
||||||
|
ViewCount = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// آپلود تصویر شاخص (اگر فایل ارسال شده باشد)
|
||||||
|
if (request.ImageFileBytes is { Length: > 0 })
|
||||||
|
{
|
||||||
|
var result = await _fileManager.UploadImageAsync(
|
||||||
|
"Images/BlogPosts",
|
||||||
|
request.ImageFileBytes,
|
||||||
|
request.ImageFileMime ?? "image/jpeg",
|
||||||
|
request.ImageFileName,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
post.FeaturedImagePath = result.Main.Path;
|
||||||
|
post.FeaturedImageThumbnailPath = result.Thumbnail.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.BlogPosts.Add(post);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// افزودن دستهبندیها
|
||||||
|
foreach (var categoryId in request.CategoryIds)
|
||||||
|
{
|
||||||
|
_context.BlogPostCategories.Add(new BlogPostCategory
|
||||||
|
{
|
||||||
|
BlogPostId = post.Id,
|
||||||
|
BlogCategoryId = categoryId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// افزودن تگها
|
||||||
|
foreach (var tagId in request.TagIds)
|
||||||
|
{
|
||||||
|
_context.BlogPostTags.Add(new BlogPostTag
|
||||||
|
{
|
||||||
|
BlogPostId = post.Id,
|
||||||
|
TagId = tagId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.CategoryIds.Any() || request.TagIds.Any())
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Blog post created. Id: {Id}, Title: {Title}, Author: {Author}",
|
||||||
|
post.Id, post.Title, authorUserId);
|
||||||
|
|
||||||
|
return post.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.CreateBlogPost;
|
||||||
|
|
||||||
|
public class CreateBlogPostCommandValidator : AbstractValidator<CreateBlogPostCommand>
|
||||||
|
{
|
||||||
|
public CreateBlogPostCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Title)
|
||||||
|
.NotEmpty().WithMessage("عنوان مقاله الزامی است")
|
||||||
|
.MaximumLength(200).WithMessage("عنوان نمیتواند بیشتر از 200 کاراکتر باشد");
|
||||||
|
|
||||||
|
RuleFor(x => x.Slug)
|
||||||
|
.NotEmpty().WithMessage("نشانی یکتا (slug) الزامی است")
|
||||||
|
.MaximumLength(200).WithMessage("نشانی نمیتواند بیشتر از 200 کاراکتر باشد")
|
||||||
|
.Matches(@"^[a-z0-9\-]+$").WithMessage("نشانی فقط میتواند شامل حروف کوچک انگلیسی، اعداد و خط تیره باشد");
|
||||||
|
|
||||||
|
RuleFor(x => x.Summary)
|
||||||
|
.MaximumLength(500).WithMessage("خلاصه نمیتواند بیشتر از 500 کاراکتر باشد")
|
||||||
|
.When(x => !string.IsNullOrEmpty(x.Summary));
|
||||||
|
|
||||||
|
RuleFor(x => x.HtmlContent)
|
||||||
|
.NotEmpty().WithMessage("محتوای مقاله الزامی است");
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.DeleteBlogPost;
|
||||||
|
|
||||||
|
public class DeleteBlogPostCommand : IRequest<Unit>
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.DeleteBlogPost;
|
||||||
|
|
||||||
|
public class DeleteBlogPostCommandHandler : IRequestHandler<DeleteBlogPostCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ILogger<DeleteBlogPostCommandHandler> _logger;
|
||||||
|
|
||||||
|
public DeleteBlogPostCommandHandler(IApplicationDbContext context, ILogger<DeleteBlogPostCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(DeleteBlogPostCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var post = await _context.BlogPosts.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
|
||||||
|
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
||||||
|
|
||||||
|
post.IsDeleted = true;
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Blog post soft-deleted. Id: {Id}", post.Id);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.IncrementViewCount;
|
||||||
|
|
||||||
|
public class IncrementViewCountCommand : IRequest<Unit>
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.IncrementViewCount;
|
||||||
|
|
||||||
|
public class IncrementViewCountCommandHandler : IRequestHandler<IncrementViewCountCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public IncrementViewCountCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(IncrementViewCountCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var post = await _context.BlogPosts.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken);
|
||||||
|
if (post != null)
|
||||||
|
{
|
||||||
|
post.ViewCount++;
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.PublishBlogPost;
|
||||||
|
|
||||||
|
public class PublishBlogPostCommand : IRequest<PublishBlogPostResult>
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PublishBlogPostResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public DateTime? PublishedAt { get; set; }
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.PublishBlogPost;
|
||||||
|
|
||||||
|
public class PublishBlogPostCommandHandler : IRequestHandler<PublishBlogPostCommand, PublishBlogPostResult>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ILogger<PublishBlogPostCommandHandler> _logger;
|
||||||
|
|
||||||
|
public PublishBlogPostCommandHandler(IApplicationDbContext context, ILogger<PublishBlogPostCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PublishBlogPostResult> Handle(PublishBlogPostCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var post = await _context.BlogPosts.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
|
||||||
|
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
||||||
|
|
||||||
|
if (post.Status == BlogPostStatus.Published)
|
||||||
|
return new PublishBlogPostResult { Success = false, Message = "مقاله قبلاً منتشر شده است" };
|
||||||
|
|
||||||
|
post.Status = BlogPostStatus.Published;
|
||||||
|
post.PublishedAt = DateTime.UtcNow;
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Blog post published. Id: {Id}, Title: {Title}", post.Id, post.Title);
|
||||||
|
|
||||||
|
return new PublishBlogPostResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "مقاله با موفقیت منتشر شد",
|
||||||
|
PublishedAt = post.PublishedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.UpdateBlogPost;
|
||||||
|
|
||||||
|
public class UpdateBlogPostCommand : IRequest<Unit>
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
public string HtmlContent { get; set; } = string.Empty;
|
||||||
|
public string? FeaturedImagePath { get; set; }
|
||||||
|
public string? FeaturedImageThumbnailPath { get; set; }
|
||||||
|
public List<long> CategoryIds { get; set; } = new();
|
||||||
|
public List<long> TagIds { get; set; } = new();
|
||||||
|
public bool IsFeatured { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
|
||||||
|
// Image upload properties
|
||||||
|
public byte[]? ImageFileBytes { get; set; }
|
||||||
|
public string? ImageFileMime { get; set; }
|
||||||
|
public string? ImageFileName { get; set; }
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities.Blog;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.UpdateBlogPost;
|
||||||
|
|
||||||
|
public class UpdateBlogPostCommandHandler : IRequestHandler<UpdateBlogPostCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IFileManager _fileManager;
|
||||||
|
private readonly ILogger<UpdateBlogPostCommandHandler> _logger;
|
||||||
|
|
||||||
|
public UpdateBlogPostCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IFileManager fileManager,
|
||||||
|
ILogger<UpdateBlogPostCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_fileManager = fileManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(UpdateBlogPostCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var post = await _context.BlogPosts.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
|
||||||
|
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
||||||
|
|
||||||
|
post.Title = request.Title.Trim();
|
||||||
|
// حفظ slug قبلی اگر مقدار جدید خالی باشد
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.Slug))
|
||||||
|
post.Slug = request.Slug.Trim().ToLower();
|
||||||
|
post.Summary = request.Summary?.Trim();
|
||||||
|
post.HtmlContent = request.HtmlContent;
|
||||||
|
post.FeaturedImagePath = request.FeaturedImagePath;
|
||||||
|
post.FeaturedImageThumbnailPath = request.FeaturedImageThumbnailPath;
|
||||||
|
post.IsFeatured = request.IsFeatured;
|
||||||
|
post.SortOrder = request.SortOrder;
|
||||||
|
|
||||||
|
// آپلود تصویر شاخص (اگر فایل جدید ارسال شده باشد)
|
||||||
|
if (request.ImageFileBytes is { Length: > 0 })
|
||||||
|
{
|
||||||
|
var result = await _fileManager.UploadImageAsync(
|
||||||
|
"Images/BlogPosts",
|
||||||
|
request.ImageFileBytes,
|
||||||
|
request.ImageFileMime ?? "image/jpeg",
|
||||||
|
request.ImageFileName,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
post.FeaturedImagePath = result.Main.Path;
|
||||||
|
post.FeaturedImageThumbnailPath = result.Thumbnail.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// بروزرسانی دستهبندیها
|
||||||
|
var existingCategories = await _context.BlogPostCategories
|
||||||
|
.Where(x => x.BlogPostId == post.Id).ToListAsync(cancellationToken);
|
||||||
|
_context.BlogPostCategories.RemoveRange(existingCategories);
|
||||||
|
foreach (var categoryId in request.CategoryIds)
|
||||||
|
{
|
||||||
|
_context.BlogPostCategories.Add(new BlogPostCategory
|
||||||
|
{
|
||||||
|
BlogPostId = post.Id,
|
||||||
|
BlogCategoryId = categoryId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// بروزرسانی تگها
|
||||||
|
var existingTags = await _context.BlogPostTags
|
||||||
|
.Where(x => x.BlogPostId == post.Id).ToListAsync(cancellationToken);
|
||||||
|
_context.BlogPostTags.RemoveRange(existingTags);
|
||||||
|
foreach (var tagId in request.TagIds)
|
||||||
|
{
|
||||||
|
_context.BlogPostTags.Add(new BlogPostTag
|
||||||
|
{
|
||||||
|
BlogPostId = post.Id,
|
||||||
|
TagId = tagId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Blog post updated. Id: {Id}, Title: {Title}", post.Id, post.Title);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Commands.UpdateBlogPost;
|
||||||
|
|
||||||
|
public class UpdateBlogPostCommandValidator : AbstractValidator<UpdateBlogPostCommand>
|
||||||
|
{
|
||||||
|
public UpdateBlogPostCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Id).GreaterThan(0).WithMessage("شناسه مقاله نامعتبر است");
|
||||||
|
|
||||||
|
RuleFor(x => x.Title)
|
||||||
|
.NotEmpty().WithMessage("عنوان مقاله الزامی است")
|
||||||
|
.MaximumLength(200).WithMessage("عنوان نمیتواند بیشتر از 200 کاراکتر باشد");
|
||||||
|
|
||||||
|
RuleFor(x => x.Slug)
|
||||||
|
.NotEmpty().WithMessage("نشانی یکتا (slug) الزامی است")
|
||||||
|
.MaximumLength(200).WithMessage("نشانی نمیتواند بیشتر از 200 کاراکتر باشد")
|
||||||
|
.Matches(@"^[a-z0-9\-]+$").WithMessage("نشانی فقط میتواند شامل حروف کوچک انگلیسی، اعداد و خط تیره باشد");
|
||||||
|
|
||||||
|
RuleFor(x => x.HtmlContent)
|
||||||
|
.NotEmpty().WithMessage("محتوای مقاله الزامی است");
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetAllBlogPosts;
|
||||||
|
|
||||||
|
public class GetAllBlogPostsQuery : IRequest<GetAllBlogPostsResponseDto>
|
||||||
|
{
|
||||||
|
public int PageNumber { get; set; } = 1;
|
||||||
|
public int PageSize { get; set; } = 10;
|
||||||
|
public string? SortBy { get; set; }
|
||||||
|
public string? SearchTerm { get; set; }
|
||||||
|
public BlogPostStatus? Status { get; set; }
|
||||||
|
public long? CategoryId { get; set; }
|
||||||
|
public bool? IsFeatured { get; set; }
|
||||||
|
}
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetAllBlogPosts;
|
||||||
|
|
||||||
|
public class GetAllBlogPostsQueryHandler : IRequestHandler<GetAllBlogPostsQuery, GetAllBlogPostsResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetAllBlogPostsQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetAllBlogPostsResponseDto> Handle(GetAllBlogPostsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var query = _context.BlogPosts
|
||||||
|
.Include(x => x.BlogPostCategories).ThenInclude(x => x.BlogCategory)
|
||||||
|
.Where(x => !x.IsDeleted);
|
||||||
|
|
||||||
|
// فیلترها
|
||||||
|
if (request.Status.HasValue)
|
||||||
|
query = query.Where(x => x.Status == request.Status.Value);
|
||||||
|
if (request.CategoryId.HasValue)
|
||||||
|
query = query.Where(x => x.BlogPostCategories.Any(c => c.BlogCategoryId == request.CategoryId.Value));
|
||||||
|
if (request.IsFeatured.HasValue)
|
||||||
|
query = query.Where(x => x.IsFeatured == request.IsFeatured.Value);
|
||||||
|
if (!string.IsNullOrEmpty(request.SearchTerm))
|
||||||
|
{
|
||||||
|
var term = request.SearchTerm.ToLower();
|
||||||
|
query = query.Where(x => x.Title.ToLower().Contains(term) || (x.Summary != null && x.Summary.ToLower().Contains(term)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
// مرتبسازی
|
||||||
|
query = request.SortBy?.ToLower() switch
|
||||||
|
{
|
||||||
|
"title" => query.OrderBy(x => x.Title),
|
||||||
|
"viewcount" => query.OrderByDescending(x => x.ViewCount),
|
||||||
|
"publishedat" => query.OrderByDescending(x => x.PublishedAt),
|
||||||
|
_ => query.OrderByDescending(x => x.Created)
|
||||||
|
};
|
||||||
|
|
||||||
|
var posts = await query
|
||||||
|
.Skip((request.PageNumber - 1) * request.PageSize)
|
||||||
|
.Take(request.PageSize)
|
||||||
|
.Select(x => new BlogPostListItemDto
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
Title = x.Title,
|
||||||
|
Slug = x.Slug,
|
||||||
|
Summary = x.Summary,
|
||||||
|
FeaturedImageThumbnailPath = x.FeaturedImageThumbnailPath,
|
||||||
|
Status = (int)x.Status,
|
||||||
|
StatusName = GetBlogPostQueryHandler.GetStatusName(x.Status),
|
||||||
|
PublishedAt = x.PublishedAt,
|
||||||
|
ViewCount = x.ViewCount,
|
||||||
|
IsFeatured = x.IsFeatured,
|
||||||
|
Created = x.Created,
|
||||||
|
Categories = x.BlogPostCategories.Select(c => new BlogPostCategoryDto
|
||||||
|
{
|
||||||
|
Id = c.BlogCategory.Id,
|
||||||
|
Title = c.BlogCategory.Title,
|
||||||
|
Slug = c.BlogCategory.Slug
|
||||||
|
}).ToList()
|
||||||
|
})
|
||||||
|
.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 GetAllBlogPostsResponseDto { MetaData = metaData, Models = posts };
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetAllBlogPosts;
|
||||||
|
|
||||||
|
public class GetAllBlogPostsResponseDto
|
||||||
|
{
|
||||||
|
public MetaData MetaData { get; set; } = new();
|
||||||
|
public List<BlogPostListItemDto> Models { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BlogPostListItemDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
public string? FeaturedImageThumbnailPath { get; set; }
|
||||||
|
public int Status { get; set; }
|
||||||
|
public string StatusName { get; set; } = string.Empty;
|
||||||
|
public DateTime? PublishedAt { get; set; }
|
||||||
|
public int ViewCount { get; set; }
|
||||||
|
public bool IsFeatured { get; set; }
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
public List<BlogPostCategoryDto> Categories { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||||
|
|
||||||
|
public class BlogPostDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
public string HtmlContent { get; set; } = string.Empty;
|
||||||
|
public string? FeaturedImagePath { get; set; }
|
||||||
|
public string? FeaturedImageThumbnailPath { get; set; }
|
||||||
|
public BlogPostStatus Status { get; set; }
|
||||||
|
public string StatusName { get; set; } = string.Empty;
|
||||||
|
public DateTime? PublishedAt { get; set; }
|
||||||
|
public int ViewCount { get; set; }
|
||||||
|
public long AuthorUserId { get; set; }
|
||||||
|
public bool IsFeatured { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
public DateTime? LastModified { get; set; }
|
||||||
|
public List<BlogPostCategoryDto> Categories { get; set; } = new();
|
||||||
|
public List<BlogPostTagDto> Tags { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BlogPostCategoryDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BlogPostTagDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||||
|
|
||||||
|
public class GetBlogPostQuery : IRequest<BlogPostDto>
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
}
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||||
|
|
||||||
|
public class GetBlogPostQueryHandler : IRequestHandler<GetBlogPostQuery, BlogPostDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetBlogPostQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BlogPostDto> Handle(GetBlogPostQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var post = await _context.BlogPosts
|
||||||
|
.Include(x => x.BlogPostCategories).ThenInclude(x => x.BlogCategory)
|
||||||
|
.Include(x => x.BlogPostTags).ThenInclude(x => x.Tag)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
|
||||||
|
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
||||||
|
|
||||||
|
return new BlogPostDto
|
||||||
|
{
|
||||||
|
Id = post.Id,
|
||||||
|
Title = post.Title,
|
||||||
|
Slug = post.Slug,
|
||||||
|
Summary = post.Summary,
|
||||||
|
HtmlContent = post.HtmlContent,
|
||||||
|
FeaturedImagePath = post.FeaturedImagePath,
|
||||||
|
FeaturedImageThumbnailPath = post.FeaturedImageThumbnailPath,
|
||||||
|
Status = post.Status,
|
||||||
|
StatusName = GetStatusName(post.Status),
|
||||||
|
PublishedAt = post.PublishedAt,
|
||||||
|
ViewCount = post.ViewCount,
|
||||||
|
AuthorUserId = post.AuthorUserId,
|
||||||
|
IsFeatured = post.IsFeatured,
|
||||||
|
SortOrder = post.SortOrder,
|
||||||
|
Created = post.Created,
|
||||||
|
LastModified = post.LastModified,
|
||||||
|
Categories = post.BlogPostCategories.Select(c => new BlogPostCategoryDto
|
||||||
|
{
|
||||||
|
Id = c.BlogCategory.Id,
|
||||||
|
Title = c.BlogCategory.Title,
|
||||||
|
Slug = c.BlogCategory.Slug
|
||||||
|
}).ToList(),
|
||||||
|
Tags = post.BlogPostTags.Select(t => new BlogPostTagDto
|
||||||
|
{
|
||||||
|
Id = t.Tag.Id,
|
||||||
|
Title = t.Tag.Title,
|
||||||
|
Name = t.Tag.Name
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetStatusName(BlogPostStatus status) => status switch
|
||||||
|
{
|
||||||
|
BlogPostStatus.Draft => "پیشنویس",
|
||||||
|
BlogPostStatus.Published => "منتشرشده",
|
||||||
|
BlogPostStatus.Scheduled => "زمانبندیشده",
|
||||||
|
BlogPostStatus.Archived => "آرشیو",
|
||||||
|
_ => "نامشخص"
|
||||||
|
};
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPostBySlug;
|
||||||
|
|
||||||
|
public class GetBlogPostBySlugQuery : IRequest<GetBlogPost.BlogPostDto>
|
||||||
|
{
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPostBySlug;
|
||||||
|
|
||||||
|
public class GetBlogPostBySlugQueryHandler : IRequestHandler<GetBlogPostBySlugQuery, BlogPostDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetBlogPostBySlugQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BlogPostDto> Handle(GetBlogPostBySlugQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var post = await _context.BlogPosts
|
||||||
|
.Include(x => x.BlogPostCategories).ThenInclude(x => x.BlogCategory)
|
||||||
|
.Include(x => x.BlogPostTags).ThenInclude(x => x.Tag)
|
||||||
|
.FirstOrDefaultAsync(x => x.Slug == request.Slug && !x.IsDeleted, cancellationToken)
|
||||||
|
?? throw new KeyNotFoundException($"مقاله با نشانی '{request.Slug}' یافت نشد");
|
||||||
|
|
||||||
|
return new BlogPostDto
|
||||||
|
{
|
||||||
|
Id = post.Id,
|
||||||
|
Title = post.Title,
|
||||||
|
Slug = post.Slug,
|
||||||
|
Summary = post.Summary,
|
||||||
|
HtmlContent = post.HtmlContent,
|
||||||
|
FeaturedImagePath = post.FeaturedImagePath,
|
||||||
|
FeaturedImageThumbnailPath = post.FeaturedImageThumbnailPath,
|
||||||
|
Status = post.Status,
|
||||||
|
StatusName = GetBlogPostQueryHandler.GetStatusName(post.Status),
|
||||||
|
PublishedAt = post.PublishedAt,
|
||||||
|
ViewCount = post.ViewCount,
|
||||||
|
AuthorUserId = post.AuthorUserId,
|
||||||
|
IsFeatured = post.IsFeatured,
|
||||||
|
SortOrder = post.SortOrder,
|
||||||
|
Created = post.Created,
|
||||||
|
LastModified = post.LastModified,
|
||||||
|
Categories = post.BlogPostCategories.Select(c => new BlogPostCategoryDto
|
||||||
|
{
|
||||||
|
Id = c.BlogCategory.Id,
|
||||||
|
Title = c.BlogCategory.Title,
|
||||||
|
Slug = c.BlogCategory.Slug
|
||||||
|
}).ToList(),
|
||||||
|
Tags = post.BlogPostTags.Select(t => new BlogPostTagDto
|
||||||
|
{
|
||||||
|
Id = t.Tag.Id,
|
||||||
|
Title = t.Tag.Title,
|
||||||
|
Name = t.Tag.Name
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetFeaturedBlogPosts;
|
||||||
|
|
||||||
|
public class GetFeaturedBlogPostsQuery : IRequest<List<GetAllBlogPosts.BlogPostListItemDto>>
|
||||||
|
{
|
||||||
|
public int Count { get; set; } = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetFeaturedBlogPostsQueryHandler : IRequestHandler<GetFeaturedBlogPostsQuery, List<GetAllBlogPosts.BlogPostListItemDto>>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetFeaturedBlogPostsQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<GetAllBlogPosts.BlogPostListItemDto>> Handle(GetFeaturedBlogPostsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var posts = await _context.BlogPosts
|
||||||
|
.Include(x => x.BlogPostCategories).ThenInclude(x => x.BlogCategory)
|
||||||
|
.Where(x => !x.IsDeleted && x.Status == BlogPostStatus.Published && x.IsFeatured)
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ThenByDescending(x => x.PublishedAt)
|
||||||
|
.Take(request.Count)
|
||||||
|
.Select(x => new GetAllBlogPosts.BlogPostListItemDto
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
Title = x.Title,
|
||||||
|
Slug = x.Slug,
|
||||||
|
Summary = x.Summary,
|
||||||
|
FeaturedImageThumbnailPath = x.FeaturedImageThumbnailPath,
|
||||||
|
Status = (int)x.Status,
|
||||||
|
StatusName = GetBlogPostQueryHandler.GetStatusName(x.Status),
|
||||||
|
PublishedAt = x.PublishedAt,
|
||||||
|
ViewCount = x.ViewCount,
|
||||||
|
IsFeatured = x.IsFeatured,
|
||||||
|
Created = x.Created,
|
||||||
|
Categories = x.BlogPostCategories.Select(c => new BlogPostCategoryDto
|
||||||
|
{
|
||||||
|
Id = c.BlogCategory.Id,
|
||||||
|
Title = c.BlogCategory.Title,
|
||||||
|
Slug = c.BlogCategory.Slug
|
||||||
|
}).ToList()
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
}
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetPublishedBlogPosts;
|
||||||
|
|
||||||
|
public class GetPublishedBlogPostsQuery : IRequest<GetAllBlogPosts.GetAllBlogPostsResponseDto>
|
||||||
|
{
|
||||||
|
public int PageNumber { get; set; } = 1;
|
||||||
|
public int PageSize { get; set; } = 10;
|
||||||
|
public string? SearchTerm { get; set; }
|
||||||
|
public long? CategoryId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetPublishedBlogPostsQueryHandler : IRequestHandler<GetPublishedBlogPostsQuery, GetAllBlogPosts.GetAllBlogPostsResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetPublishedBlogPostsQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetAllBlogPosts.GetAllBlogPostsResponseDto> Handle(GetPublishedBlogPostsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var query = _context.BlogPosts
|
||||||
|
.Include(x => x.BlogPostCategories).ThenInclude(x => x.BlogCategory)
|
||||||
|
.Where(x => !x.IsDeleted && x.Status == BlogPostStatus.Published);
|
||||||
|
|
||||||
|
if (request.CategoryId.HasValue)
|
||||||
|
query = query.Where(x => x.BlogPostCategories.Any(c => c.BlogCategoryId == request.CategoryId.Value));
|
||||||
|
if (!string.IsNullOrEmpty(request.SearchTerm))
|
||||||
|
{
|
||||||
|
var term = request.SearchTerm.ToLower();
|
||||||
|
query = query.Where(x => x.Title.ToLower().Contains(term) || (x.Summary != null && x.Summary.ToLower().Contains(term)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var posts = await query
|
||||||
|
.OrderByDescending(x => x.PublishedAt)
|
||||||
|
.Skip((request.PageNumber - 1) * request.PageSize)
|
||||||
|
.Take(request.PageSize)
|
||||||
|
.Select(x => new GetAllBlogPosts.BlogPostListItemDto
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
Title = x.Title,
|
||||||
|
Slug = x.Slug,
|
||||||
|
Summary = x.Summary,
|
||||||
|
FeaturedImageThumbnailPath = x.FeaturedImageThumbnailPath,
|
||||||
|
Status = (int)x.Status,
|
||||||
|
StatusName = GetBlogPostQueryHandler.GetStatusName(x.Status),
|
||||||
|
PublishedAt = x.PublishedAt,
|
||||||
|
ViewCount = x.ViewCount,
|
||||||
|
IsFeatured = x.IsFeatured,
|
||||||
|
Created = x.Created,
|
||||||
|
Categories = x.BlogPostCategories.Select(c => new BlogPostCategoryDto
|
||||||
|
{
|
||||||
|
Id = c.BlogCategory.Id,
|
||||||
|
Title = c.BlogCategory.Title,
|
||||||
|
Slug = c.BlogCategory.Slug
|
||||||
|
}).ToList()
|
||||||
|
})
|
||||||
|
.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 GetAllBlogPosts.GetAllBlogPostsResponseDto { MetaData = metaData, Models = posts };
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.AddBlogPostImage;
|
||||||
|
|
||||||
|
public class AddBlogPostImageCommand : IRequest<long>
|
||||||
|
{
|
||||||
|
public long BlogPostId { get; set; }
|
||||||
|
public string ImagePath { get; set; } = default!;
|
||||||
|
public string ThumbnailPath { get; set; } = default!;
|
||||||
|
public string? AltText { get; set; }
|
||||||
|
public string? Caption { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities.Blog;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.AddBlogPostImage;
|
||||||
|
|
||||||
|
public class AddBlogPostImageCommandHandler : IRequestHandler<AddBlogPostImageCommand, long>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public AddBlogPostImageCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> Handle(AddBlogPostImageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var blogPost = await _context.BlogPosts.FirstOrDefaultAsync(x => x.Id == request.BlogPostId && !x.IsDeleted, cancellationToken);
|
||||||
|
if (blogPost == null)
|
||||||
|
throw new NotFoundException(nameof(BlogPost), request.BlogPostId);
|
||||||
|
|
||||||
|
var entity = new BlogPostImage
|
||||||
|
{
|
||||||
|
BlogPostId = request.BlogPostId,
|
||||||
|
ImagePath = request.ImagePath,
|
||||||
|
ThumbnailPath = request.ThumbnailPath,
|
||||||
|
AltText = request.AltText,
|
||||||
|
Caption = request.Caption,
|
||||||
|
SortOrder = request.SortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.BlogPostImages.Add(entity);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return entity.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.AddBlogPostImage;
|
||||||
|
|
||||||
|
public class AddBlogPostImageCommandValidator : AbstractValidator<AddBlogPostImageCommand>
|
||||||
|
{
|
||||||
|
public AddBlogPostImageCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.BlogPostId)
|
||||||
|
.GreaterThan(0).WithMessage("شناسه پست نامعتبر است");
|
||||||
|
|
||||||
|
RuleFor(x => x.ImagePath)
|
||||||
|
.NotEmpty().WithMessage("مسیر تصویر الزامی است");
|
||||||
|
|
||||||
|
RuleFor(x => x.AltText)
|
||||||
|
.MaximumLength(500).WithMessage("متن جایگزین حداکثر ۵۰۰ کاراکتر");
|
||||||
|
|
||||||
|
RuleFor(x => x.Caption)
|
||||||
|
.MaximumLength(1000).WithMessage("عنوان تصویر حداکثر ۱۰۰۰ کاراکتر");
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.DeleteBlogPostImage;
|
||||||
|
|
||||||
|
public class DeleteBlogPostImageCommand : 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.BlogPostImageCQ.Commands.DeleteBlogPostImage;
|
||||||
|
|
||||||
|
public class DeleteBlogPostImageCommandHandler : IRequestHandler<DeleteBlogPostImageCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public DeleteBlogPostImageCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(DeleteBlogPostImageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await _context.BlogPostImages.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||||
|
if (entity == null || entity.IsDeleted)
|
||||||
|
throw new NotFoundException(nameof(BlogPostImage), request.Id);
|
||||||
|
|
||||||
|
entity.IsDeleted = true;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.ReorderBlogPostImages;
|
||||||
|
|
||||||
|
public class ReorderBlogPostImagesCommand : IRequest<Unit>
|
||||||
|
{
|
||||||
|
public List<ImageSortItem> Items { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImageSortItem
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.ReorderBlogPostImages;
|
||||||
|
|
||||||
|
public class ReorderBlogPostImagesCommandHandler : IRequestHandler<ReorderBlogPostImagesCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public ReorderBlogPostImagesCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(ReorderBlogPostImagesCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var ids = request.Items.Select(x => x.Id).ToList();
|
||||||
|
var images = await _context.BlogPostImages
|
||||||
|
.Where(x => ids.Contains(x.Id) && !x.IsDeleted)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
foreach (var item in request.Items)
|
||||||
|
{
|
||||||
|
var image = images.FirstOrDefault(x => x.Id == item.Id);
|
||||||
|
if (image != null)
|
||||||
|
image.SortOrder = item.SortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostImageCQ.Queries.GetBlogPostImages;
|
||||||
|
|
||||||
|
public class GetBlogPostImagesQuery : IRequest<List<BlogPostImageDto>>
|
||||||
|
{
|
||||||
|
public long BlogPostId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BlogPostImageDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public long BlogPostId { get; set; }
|
||||||
|
public string ImagePath { get; set; } = default!;
|
||||||
|
public string? ThumbnailPath { get; set; }
|
||||||
|
public string? AltText { get; set; }
|
||||||
|
public string? Caption { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.BlogPostImageCQ.Queries.GetBlogPostImages;
|
||||||
|
|
||||||
|
public class GetBlogPostImagesQueryHandler : IRequestHandler<GetBlogPostImagesQuery, List<BlogPostImageDto>>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetBlogPostImagesQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<BlogPostImageDto>> Handle(GetBlogPostImagesQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var images = await _context.BlogPostImages
|
||||||
|
.Where(x => x.BlogPostId == request.BlogPostId && !x.IsDeleted)
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.Select(x => new BlogPostImageDto
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
BlogPostId = x.BlogPostId,
|
||||||
|
ImagePath = x.ImagePath,
|
||||||
|
ThumbnailPath = x.ThumbnailPath,
|
||||||
|
AltText = x.AltText,
|
||||||
|
Caption = x.Caption,
|
||||||
|
SortOrder = x.SortOrder
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return images;
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-1
@@ -31,7 +31,18 @@ public class GetAllCategoryByFilterQueryHandler : IRequestHandler<GetAllCategory
|
|||||||
{
|
{
|
||||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||||
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
||||||
.ProjectToType<GetAllCategoryByFilterResponseModel>().ToListAsync(cancellationToken)
|
.Select(x => new GetAllCategoryByFilterResponseModel
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
Name = x.Name,
|
||||||
|
Title = x.Title,
|
||||||
|
Description = x.Description,
|
||||||
|
ImagePath = x.ImagePath,
|
||||||
|
ParentId = x.ParentId,
|
||||||
|
IsActive = x.IsActive,
|
||||||
|
SortOrder = x.SortOrder,
|
||||||
|
ProductCount = x.ProductCategories.Count
|
||||||
|
}).ToListAsync(cancellationToken)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -24,4 +24,6 @@ public class GetAllCategoryByFilterResponseDto
|
|||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
//ترتیب نمایش
|
//ترتیب نمایش
|
||||||
public int SortOrder { get; set; }
|
public int SortOrder { get; set; }
|
||||||
|
//تعداد محصولات
|
||||||
|
public int ProductCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-4
@@ -53,20 +53,28 @@ public class AcceptClubMembershipContractCommandHandler
|
|||||||
AcceptClubMembershipContractCommand request,
|
AcceptClubMembershipContractCommand request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// خواندن UserId از JWT (امنتر از دریافت از کلاینت)
|
||||||
|
if (!long.TryParse(_currentUser.UserId, out var userId))
|
||||||
|
return new AcceptClubMembershipContractResponseDto
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "کاربر احراز هویت نشده است"
|
||||||
|
};
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Processing club membership contract for UserId: {UserId}",
|
"Processing club membership contract for UserId: {UserId}",
|
||||||
request.UserId
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
// 1. دریافت کاربر
|
// 1. دریافت کاربر
|
||||||
var user = await _context.Users
|
var user = await _context.Users
|
||||||
.Include(u => u.ClubMembership)
|
.Include(u => u.ClubMembership)
|
||||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("User not found: {UserId}", request.UserId);
|
_logger.LogWarning("User not found: {UserId}", userId);
|
||||||
throw new NotFoundException(nameof(User), request.UserId);
|
throw new NotFoundException(nameof(User), userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. بررسی خرید پکیج
|
// 2. بررسی خرید پکیج
|
||||||
|
|||||||
-4
@@ -4,10 +4,6 @@ public class AcceptClubMembershipContractCommandValidator : AbstractValidator<Ac
|
|||||||
{
|
{
|
||||||
public AcceptClubMembershipContractCommandValidator()
|
public AcceptClubMembershipContractCommandValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.UserId)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("شناسه کاربر الزامی است");
|
|
||||||
|
|
||||||
RuleFor(x => x.OtpCode)
|
RuleFor(x => x.OtpCode)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithMessage("کد تایید الزامی است")
|
.WithMessage("کد تایید الزامی است")
|
||||||
|
|||||||
+4
-4
@@ -6,14 +6,14 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools;
|
|||||||
public record GetAllWeeklyPoolsQuery : IRequest<GetAllWeeklyPoolsResponseDto>
|
public record GetAllWeeklyPoolsQuery : IRequest<GetAllWeeklyPoolsResponseDto>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// از هفته (فیلتر اختیاری)
|
/// از هفته — WeekDefinitionId (فیلتر اختیاری)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? FromWeekOrder { get; init; }
|
public long? FromWeekDefinitionId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// تا هفته (فیلتر اختیاری)
|
/// تا هفته — WeekDefinitionId (فیلتر اختیاری)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ToWeekOrder { get; init; }
|
public long? ToWeekDefinitionId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// فقط Pool های محاسبه شده
|
/// فقط Pool های محاسبه شده
|
||||||
|
|||||||
+4
-4
@@ -20,14 +20,14 @@ public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQu
|
|||||||
.AsNoTracking();
|
.AsNoTracking();
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
if (request.FromWeekOrder!=null)
|
if (request.FromWeekDefinitionId != null)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.WeekDefinition.WeekOrder>=request.FromWeekOrder );
|
query = query.Where(x => x.WeekDefinitionId >= request.FromWeekDefinitionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.ToWeekOrder!=null)
|
if (request.ToWeekDefinitionId != null)
|
||||||
{
|
{
|
||||||
query = query.Where(x =>x.WeekDefinition.WeekOrder<= request.ToWeekOrder);
|
query = query.Where(x => x.WeekDefinitionId <= request.ToWeekDefinitionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.OnlyCalculated.HasValue && request.OnlyCalculated.Value)
|
if (request.OnlyCalculated.HasValue && request.OnlyCalculated.Value)
|
||||||
|
|||||||
+4
-13
@@ -4,16 +4,13 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
private readonly ICurrentUserService _currentUser;
|
|
||||||
|
|
||||||
public GetUserCommissionPayoutsQueryHandler(
|
public GetUserCommissionPayoutsQueryHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IWeekDefinitionRepository weekDefinitionRepository,
|
IWeekDefinitionRepository weekDefinitionRepository)
|
||||||
ICurrentUserService currentUser)
|
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_weekDefinitionRepository = weekDefinitionRepository;
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
_currentUser = currentUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetUserCommissionPayoutsResponseDto> Handle(GetUserCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
public async Task<GetUserCommissionPayoutsResponseDto> Handle(GetUserCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
||||||
@@ -24,17 +21,11 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
// اگر UserId داده نشده، از CurrentUser بگیر (برای Customer API)
|
// UserId > 0 → filter by that user
|
||||||
|
// UserId == 0 or null → show ALL users (admin mode)
|
||||||
|
// Customer endpoints resolve UserId from JWT before calling this handler
|
||||||
long? userId = request.UserId;
|
long? userId = request.UserId;
|
||||||
if (!userId.HasValue || userId.Value == 0)
|
|
||||||
{
|
|
||||||
if (long.TryParse(_currentUser.UserId, out var currentUserId))
|
|
||||||
{
|
|
||||||
userId = currentUserId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// فیلترها
|
|
||||||
if (userId.HasValue && userId.Value > 0)
|
if (userId.HasValue && userId.Value > 0)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.UserId == userId.Value);
|
query = query.Where(x => x.UserId == userId.Value);
|
||||||
|
|||||||
+4
-13
@@ -4,16 +4,13 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
private readonly ICurrentUserService _currentUser;
|
|
||||||
|
|
||||||
public GetUserWeeklyBalancesQueryHandler(
|
public GetUserWeeklyBalancesQueryHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IWeekDefinitionRepository weekDefinitionRepository,
|
IWeekDefinitionRepository weekDefinitionRepository)
|
||||||
ICurrentUserService currentUser)
|
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_weekDefinitionRepository = weekDefinitionRepository;
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
_currentUser = currentUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetUserWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetUserWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
||||||
@@ -24,17 +21,11 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
// اگر UserId داده نشده، از CurrentUser بگیر (برای Customer API)
|
// UserId > 0 → filter by that user
|
||||||
|
// UserId == 0 or null → show ALL users (admin mode)
|
||||||
|
// Customer endpoints resolve UserId from JWT before calling this handler
|
||||||
long? userId = request.UserId;
|
long? userId = request.UserId;
|
||||||
if (!userId.HasValue || userId.Value == 0)
|
|
||||||
{
|
|
||||||
if (long.TryParse(_currentUser.UserId, out var currentUserId))
|
|
||||||
{
|
|
||||||
userId = currentUserId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// فیلترها
|
|
||||||
if (userId.HasValue && userId.Value > 0)
|
if (userId.HasValue && userId.Value > 0)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.UserId == userId.Value);
|
query = query.Where(x => x.UserId == userId.Value);
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ public class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest> where T
|
|||||||
{
|
{
|
||||||
var requestName = typeof(TRequest).Name;
|
var requestName = typeof(TRequest).Name;
|
||||||
var userId = _currentUserService.UserId ?? string.Empty;
|
var userId = _currentUserService.UserId ?? string.Empty;
|
||||||
_logger.LogInformation("Request: {Name} {@UserId} {@Request}",
|
var safeLog = request?.ToString() ?? "";
|
||||||
requestName, userId, request);
|
if (safeLog.Length > 2000)
|
||||||
|
safeLog = safeLog[..2000] + "... [TRUNCATED]";
|
||||||
|
_logger.LogInformation("Request: {Name} {UserId} {Request}",
|
||||||
|
requestName, userId, safeLog);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.Common.Behaviours;
|
namespace CMSMicroservice.Application.Common.Behaviours;
|
||||||
|
|
||||||
public class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
public partial class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||||
where TRequest : IRequest<TResponse>
|
where TRequest : IRequest<TResponse>
|
||||||
{
|
{
|
||||||
private readonly Stopwatch _timer;
|
private readonly Stopwatch _timer;
|
||||||
private readonly ILogger<TRequest> _logger;
|
private readonly ILogger<TRequest> _logger;
|
||||||
private readonly ICurrentUserService _currentUserService;
|
private readonly ICurrentUserService _currentUserService;
|
||||||
|
|
||||||
|
[GeneratedRegex(@"ImageFile(?:Bytes|Mime|FileName)|ImageFile|File", RegexOptions.None)]
|
||||||
|
private static partial Regex BinaryPropertyPattern();
|
||||||
|
|
||||||
public PerformanceBehaviour(ILogger<TRequest> logger, ICurrentUserService currentUserService)
|
public PerformanceBehaviour(ILogger<TRequest> logger, ICurrentUserService currentUserService)
|
||||||
{
|
{
|
||||||
_timer = new Stopwatch();
|
_timer = new Stopwatch();
|
||||||
@@ -33,11 +37,20 @@ public class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequ
|
|||||||
{
|
{
|
||||||
var requestName = typeof(TRequest).Name;
|
var requestName = typeof(TRequest).Name;
|
||||||
var userId = _currentUserService.UserId ?? string.Empty;
|
var userId = _currentUserService.UserId ?? string.Empty;
|
||||||
|
var safeLog = SanitizeForLog(request);
|
||||||
|
|
||||||
_logger.LogWarning("Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@Request}",
|
_logger.LogWarning("Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {UserId} {Request}",
|
||||||
requestName, elapsedMilliseconds, userId, request);
|
requestName, elapsedMilliseconds, userId, safeLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string SanitizeForLog(TRequest request)
|
||||||
|
{
|
||||||
|
var text = request?.ToString() ?? "";
|
||||||
|
if (text.Length > 2000)
|
||||||
|
return text[..2000] + "... [TRUNCATED]";
|
||||||
|
return text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ public class UnhandledExceptionBehaviour<TRequest, TResponse> : IPipelineBehavio
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var requestName = typeof(TRequest).Name;
|
var requestName = typeof(TRequest).Name;
|
||||||
|
var safeLog = request?.ToString() ?? "";
|
||||||
|
if (safeLog.Length > 2000)
|
||||||
|
safeLog = safeLog[..2000] + "... [TRUNCATED]";
|
||||||
|
|
||||||
_logger.LogError(ex, "Request: Unhandled Exception for Request {Name} {@Request}", requestName, request);
|
_logger.LogError(ex, "Request: Unhandled Exception for Request {Name} {Request}", requestName, safeLog);
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.FileManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس مدیریت فایل — ذخیره روی دیسک، مسیر نسبی در دیتابیس
|
||||||
|
/// موقع واکشی: خواندن از دیسک و تبدیل به base64 data-URI
|
||||||
|
/// </summary>
|
||||||
|
public interface IFileManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// آپلود یک فایل خام به دیسک
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>نتیجه آپلود شامل مسیر نسبی فایل</returns>
|
||||||
|
/// <exception cref="FileUploadException">در صورت خطای آپلود</exception>
|
||||||
|
Task<UploadedFile> UploadAsync(
|
||||||
|
string directory,
|
||||||
|
byte[] fileBytes,
|
||||||
|
string mime,
|
||||||
|
string? fileName = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آپلود تصویر با بهینهسازی خودکار + ساخت بندانگشتی
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>نتیجه آپلود شامل تصویر اصلی و بندانگشتی (مسیرهای نسبی)</returns>
|
||||||
|
/// <exception cref="FileUploadException">در صورت خطای آپلود</exception>
|
||||||
|
Task<UploadedImage> UploadImageAsync(
|
||||||
|
string directory,
|
||||||
|
byte[] fileBytes,
|
||||||
|
string mime,
|
||||||
|
string? fileName = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حذف فایل از دیسک
|
||||||
|
/// </summary>
|
||||||
|
Task DeleteAsync(long fileId, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// خواندن فایل از دیسک و تبدیل به base64 data-URI
|
||||||
|
/// اگر مسیر از قبل data: باشد، همان را برمیگرداند
|
||||||
|
/// اگر فایل وجود نداشته باشد، رشته خالی برمیگرداند
|
||||||
|
/// </summary>
|
||||||
|
string ResolveImageUrl(string? path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نتیجه آپلود فایل
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">شناسه فایل در FMS</param>
|
||||||
|
/// <param name="Path">مسیر فایل ذخیرهشده (مثلاً /Images/Products/abc.jpg)</param>
|
||||||
|
public sealed record UploadedFile(long Id, string Path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نتیجه آپلود تصویر — شامل تصویر اصلی و بندانگشتی
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Main">تصویر اصلی بهینهشده</param>
|
||||||
|
/// <param name="Thumbnail">تصویر بندانگشتی</param>
|
||||||
|
public sealed record UploadedImage(UploadedFile Main, UploadedFile Thumbnail);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// خطای آپلود فایل
|
||||||
|
/// </summary>
|
||||||
|
public class FileUploadException : Exception
|
||||||
|
{
|
||||||
|
public FileUploadException(string message) : base(message) { }
|
||||||
|
public FileUploadException(string message, Exception inner) : base(message, inner) { }
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using CMSMicroservice.Domain.Entities.Blog;
|
||||||
|
using CMSMicroservice.Domain.Entities.Content;
|
||||||
using CMSMicroservice.Domain.Entities.Payment;
|
using CMSMicroservice.Domain.Entities.Payment;
|
||||||
using CMSMicroservice.Domain.Entities.Order;
|
using CMSMicroservice.Domain.Entities.Order;
|
||||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||||
@@ -32,6 +34,7 @@ public interface IApplicationDbContext
|
|||||||
DbSet<UserWallet> UserWallets { get; }
|
DbSet<UserWallet> UserWallets { get; }
|
||||||
DbSet<UserWalletChangeLog> UserWalletChangeLogs { get; }
|
DbSet<UserWalletChangeLog> UserWalletChangeLogs { get; }
|
||||||
DbSet<ManualPayment> ManualPayments { get; }
|
DbSet<ManualPayment> ManualPayments { get; }
|
||||||
|
DbSet<PaymentTransaction> PaymentTransactions { get; }
|
||||||
DbSet<PublicMessage> PublicMessages { get; }
|
DbSet<PublicMessage> PublicMessages { get; }
|
||||||
DbSet<ClubMembership> ClubMemberships { get; }
|
DbSet<ClubMembership> ClubMemberships { get; }
|
||||||
DbSet<ClubMembershipHistory> ClubMembershipHistories { get; }
|
DbSet<ClubMembershipHistory> ClubMembershipHistories { get; }
|
||||||
@@ -66,6 +69,17 @@ public interface IApplicationDbContext
|
|||||||
DbSet<InventoryItem> InventoryItems { get; }
|
DbSet<InventoryItem> InventoryItems { get; }
|
||||||
DbSet<StockMovement> StockMovements { get; }
|
DbSet<StockMovement> StockMovements { get; }
|
||||||
|
|
||||||
|
// ============= Blog =============
|
||||||
|
DbSet<BlogPost> BlogPosts { get; }
|
||||||
|
DbSet<BlogCategory> BlogCategories { get; }
|
||||||
|
DbSet<BlogPostCategory> BlogPostCategories { get; }
|
||||||
|
DbSet<BlogPostTag> BlogPostTags { get; }
|
||||||
|
DbSet<BlogPostImage> BlogPostImages { get; }
|
||||||
|
|
||||||
|
// ============= Content Management =============
|
||||||
|
DbSet<SitePage> SitePages { get; }
|
||||||
|
DbSet<SitePageSection> SitePageSections { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures
|
/// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Service for uploading files to FMS (File Management Service)
|
|
||||||
/// </summary>
|
|
||||||
public interface IFileManagementService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Uploads a file to FMS and returns the stored file path
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="directory">Target directory path (e.g. "Images/Products")</param>
|
|
||||||
/// <param name="fileBytes">Raw file bytes</param>
|
|
||||||
/// <param name="mime">MIME type (e.g. "image/jpeg")</param>
|
|
||||||
/// <param name="fileName">Original file name</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <returns>The stored file path returned by FMS, or null if upload failed</returns>
|
|
||||||
Task<string?> UploadFileAsync(string directory, byte[] fileBytes, string mime, string? fileName, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Uploads an image to FMS with optimization (resize + compress)
|
|
||||||
/// Returns both main image path and thumbnail path
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="directory">Target directory path (e.g. "Images/Products")</param>
|
|
||||||
/// <param name="fileBytes">Raw image bytes</param>
|
|
||||||
/// <param name="mime">MIME type (e.g. "image/jpeg")</param>
|
|
||||||
/// <param name="fileName">Original file name</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <returns>Tuple of (mainImagePath, thumbnailPath), either can be null if upload failed</returns>
|
|
||||||
Task<(string? MainImagePath, string? ThumbnailPath)> UploadImageWithThumbnailAsync(
|
|
||||||
string directory, byte[] fileBytes, string mime, string? fileName,
|
|
||||||
CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes a file from FMS by its ID
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> DeleteFileAsync(long fileId, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
@@ -27,6 +27,24 @@ public interface IPaymentGatewayService
|
|||||||
string verificationToken,
|
string verificationToken,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تأیید پرداخت با مبلغ — برای درگاههایی مثل زرینپال که مبلغ را در Verify نیاز دارند
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="refId">شماره مرجع تراکنش (Authority در زرینپال)</param>
|
||||||
|
/// <param name="verificationToken">توکن تأیید از درگاه (Status در زرینپال)</param>
|
||||||
|
/// <param name="amountInToman">مبلغ تراکنش به تومان</param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns>وضعیت نهایی تراکنش</returns>
|
||||||
|
Task<PaymentVerificationResult> VerifyPaymentAsync(
|
||||||
|
string refId,
|
||||||
|
string verificationToken,
|
||||||
|
decimal amountInToman,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// پیشفرض: درگاههایی که Amount نمیخواهند، از overload بدون amount استفاده کنند
|
||||||
|
return VerifyPaymentAsync(refId, verificationToken, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// واریز مبلغ به حساب کاربر (برداشت از کیف پول)
|
/// واریز مبلغ به حساب کاربر (برداشت از کیف پول)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -124,6 +142,21 @@ public class PaymentVerificationResult
|
|||||||
/// پیام
|
/// پیام
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Message { get; set; }
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شماره کارت ماسکشده (مثلاً 6037-****-****-1234)
|
||||||
|
/// </summary>
|
||||||
|
public string? CardPan { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// هش کارت بانکی
|
||||||
|
/// </summary>
|
||||||
|
public string? CardHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// کد وضعیت verify از درگاه (100=موفق، 101=تکراری)
|
||||||
|
/// </summary>
|
||||||
|
public int? VerificationCode { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+5
@@ -9,4 +9,9 @@ public class AddDiscountProductImageCommand : IRequest<long>
|
|||||||
public string ThumbnailPath { get; set; } = string.Empty;
|
public string ThumbnailPath { get; set; } = string.Empty;
|
||||||
public string? Title { get; set; }
|
public string? Title { get; set; }
|
||||||
public string? AltText { get; set; }
|
public string? AltText { get; set; }
|
||||||
|
|
||||||
|
// Image file upload
|
||||||
|
public byte[]? ImageFileBytes { get; set; }
|
||||||
|
public string? ImageFileMime { get; set; }
|
||||||
|
public string? ImageFileName { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-3
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -8,10 +9,12 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.AddDiscountProduct
|
|||||||
public class AddDiscountProductImageCommandHandler : IRequestHandler<AddDiscountProductImageCommand, long>
|
public class AddDiscountProductImageCommandHandler : IRequestHandler<AddDiscountProductImageCommand, long>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IFileManager _fileManager;
|
||||||
|
|
||||||
public AddDiscountProductImageCommandHandler(IApplicationDbContext context)
|
public AddDiscountProductImageCommandHandler(IApplicationDbContext context, IFileManager fileManager)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_fileManager = fileManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<long> Handle(AddDiscountProductImageCommand request, CancellationToken cancellationToken)
|
public async Task<long> Handle(AddDiscountProductImageCommand request, CancellationToken cancellationToken)
|
||||||
@@ -23,6 +26,23 @@ public class AddDiscountProductImageCommandHandler : IRequestHandler<AddDiscount
|
|||||||
if (!productExists)
|
if (!productExists)
|
||||||
throw new InvalidOperationException($"DiscountProduct with Id {request.DiscountProductId} not found.");
|
throw new InvalidOperationException($"DiscountProduct with Id {request.DiscountProductId} not found.");
|
||||||
|
|
||||||
|
var imagePath = request.ImagePath;
|
||||||
|
var thumbnailPath = request.ThumbnailPath;
|
||||||
|
|
||||||
|
// آپلود تصویر (اگر فایل ارسال شده باشد)
|
||||||
|
if (request.ImageFileBytes is { Length: > 0 })
|
||||||
|
{
|
||||||
|
var result = await _fileManager.UploadImageAsync(
|
||||||
|
"Images/DiscountProducts/Gallery",
|
||||||
|
request.ImageFileBytes,
|
||||||
|
request.ImageFileMime ?? "image/jpeg",
|
||||||
|
request.ImageFileName,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
imagePath = result.Main.Path;
|
||||||
|
thumbnailPath = result.Thumbnail.Path;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the max sort order for this product
|
// Get the max sort order for this product
|
||||||
var maxSortOrder = await _context.DiscountProductImages
|
var maxSortOrder = await _context.DiscountProductImages
|
||||||
.Where(i => i.DiscountProductId == request.DiscountProductId)
|
.Where(i => i.DiscountProductId == request.DiscountProductId)
|
||||||
@@ -31,8 +51,8 @@ public class AddDiscountProductImageCommandHandler : IRequestHandler<AddDiscount
|
|||||||
var image = new DiscountProductImage
|
var image = new DiscountProductImage
|
||||||
{
|
{
|
||||||
DiscountProductId = request.DiscountProductId,
|
DiscountProductId = request.DiscountProductId,
|
||||||
ImagePath = request.ImagePath,
|
ImagePath = imagePath,
|
||||||
ThumbnailPath = request.ThumbnailPath,
|
ThumbnailPath = thumbnailPath,
|
||||||
Title = request.Title,
|
Title = request.Title,
|
||||||
AltText = request.AltText,
|
AltText = request.AltText,
|
||||||
SortOrder = maxSortOrder + 1,
|
SortOrder = maxSortOrder + 1,
|
||||||
|
|||||||
+2
-1
@@ -56,7 +56,7 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
|
|||||||
// Update order
|
// Update order
|
||||||
order.PaymentStatus = PaymentStatus.Success;
|
order.PaymentStatus = PaymentStatus.Success;
|
||||||
order.PaymentDate = DateTime.Now;
|
order.PaymentDate = DateTime.Now;
|
||||||
order.DeliveryStatus = DeliveryStatus.InTransit;
|
order.DeliveryStatus = DeliveryStatus.Pending;
|
||||||
|
|
||||||
// Deduct discount balance from user wallet
|
// Deduct discount balance from user wallet
|
||||||
var userWallet = await _context.UserWallets
|
var userWallet = await _context.UserWallets
|
||||||
@@ -105,6 +105,7 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
|
|||||||
|
|
||||||
transaction.PaymentStatus = PaymentStatus.Reject;
|
transaction.PaymentStatus = PaymentStatus.Reject;
|
||||||
order.PaymentStatus = PaymentStatus.Reject;
|
order.PaymentStatus = PaymentStatus.Reject;
|
||||||
|
order.DeliveryStatus = DeliveryStatus.Cancelled;
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
|||||||
+10
@@ -11,5 +11,15 @@ public class CreateDiscountProductCommand : IRequest<long>
|
|||||||
public int MaxDiscountPercent { get; set; }
|
public int MaxDiscountPercent { get; set; }
|
||||||
public string ImagePath { get; set; }
|
public string ImagePath { get; set; }
|
||||||
public string ThumbnailPath { get; set; }
|
public string ThumbnailPath { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
public List<long> CategoryIds { get; set; } = new();
|
public List<long> CategoryIds { get; set; } = new();
|
||||||
|
|
||||||
|
// Image file upload
|
||||||
|
public byte[]? ImageFileBytes { get; set; }
|
||||||
|
public string? ImageFileMime { get; set; }
|
||||||
|
public string? ImageFileName { get; set; }
|
||||||
|
public byte[]? ThumbnailFileBytes { get; set; }
|
||||||
|
public string? ThumbnailFileMime { get; set; }
|
||||||
|
public string? ThumbnailFileName { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-5
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||||
using CMSMicroservice.Domain.Enums;
|
using CMSMicroservice.Domain.Enums;
|
||||||
@@ -10,13 +11,16 @@ public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscoun
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IInventoryService _inventoryService;
|
private readonly IInventoryService _inventoryService;
|
||||||
|
private readonly IFileManager _fileManager;
|
||||||
|
|
||||||
public CreateDiscountProductCommandHandler(
|
public CreateDiscountProductCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IInventoryService inventoryService)
|
IInventoryService inventoryService,
|
||||||
|
IFileManager fileManager)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_inventoryService = inventoryService;
|
_inventoryService = inventoryService;
|
||||||
|
_fileManager = fileManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<long> Handle(CreateDiscountProductCommand request, CancellationToken cancellationToken)
|
public async Task<long> Handle(CreateDiscountProductCommand request, CancellationToken cancellationToken)
|
||||||
@@ -28,15 +32,42 @@ public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscoun
|
|||||||
FullInformation = request.FullInformation,
|
FullInformation = request.FullInformation,
|
||||||
Price = request.Price,
|
Price = request.Price,
|
||||||
MaxDiscountPercent = request.MaxDiscountPercent,
|
MaxDiscountPercent = request.MaxDiscountPercent,
|
||||||
ImagePath = request.ImagePath,
|
ImagePath = request.ImagePath ?? string.Empty,
|
||||||
ThumbnailPath = request.ThumbnailPath,
|
ThumbnailPath = request.ThumbnailPath ?? string.Empty,
|
||||||
RemainingCount = 0, // موجودی اولیه صفر - باید از طریق Inventory اضافه شود
|
RemainingCount = 0,
|
||||||
Rate = 0,
|
Rate = 0,
|
||||||
SaleCount = 0,
|
SaleCount = 0,
|
||||||
ViewCount = 0,
|
ViewCount = 0,
|
||||||
IsActive = true
|
IsActive = request.IsActive
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// آپلود تصویر اصلی (اگر فایل ارسال شده باشد)
|
||||||
|
if (request.ImageFileBytes is { Length: > 0 })
|
||||||
|
{
|
||||||
|
var result = await _fileManager.UploadImageAsync(
|
||||||
|
"Images/DiscountProducts",
|
||||||
|
request.ImageFileBytes,
|
||||||
|
request.ImageFileMime ?? "image/jpeg",
|
||||||
|
request.ImageFileName,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
product.ImagePath = result.Main.Path;
|
||||||
|
product.ThumbnailPath = result.Thumbnail.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// آپلود بندانگشتی جداگانه (اختیاری)
|
||||||
|
if (request.ThumbnailFileBytes is { Length: > 0 })
|
||||||
|
{
|
||||||
|
var thumbResult = await _fileManager.UploadAsync(
|
||||||
|
"Images/DiscountProducts/Thumbnails",
|
||||||
|
request.ThumbnailFileBytes,
|
||||||
|
request.ThumbnailFileMime ?? "image/jpeg",
|
||||||
|
request.ThumbnailFileName,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
product.ThumbnailPath = thumbResult.Path;
|
||||||
|
}
|
||||||
|
|
||||||
_context.DiscountProducts.Add(product);
|
_context.DiscountProducts.Add(product);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
|||||||
+5
@@ -18,4 +18,9 @@ public class PlaceOrderResponseDto
|
|||||||
public long TotalAmount { get; set; }
|
public long TotalAmount { get; set; }
|
||||||
public long DiscountBalanceUsed { get; set; }
|
public long DiscountBalanceUsed { get; set; }
|
||||||
public long GatewayAmountRequired { get; set; }
|
public long GatewayAmountRequired { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL درگاه پرداخت — اگر null باشد یعنی نیاز به پرداخت آنلاین نیست
|
||||||
|
/// </summary>
|
||||||
|
public string? PaymentUrl { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+125
-14
@@ -5,6 +5,8 @@ using CMSMicroservice.Domain.Entities.Payment;
|
|||||||
using CMSMicroservice.Domain.Enums;
|
using CMSMicroservice.Domain.Enums;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.PlaceOrder;
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.PlaceOrder;
|
||||||
|
|
||||||
@@ -12,13 +14,22 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IInventoryService _inventoryService;
|
private readonly IInventoryService _inventoryService;
|
||||||
|
private readonly IPaymentGatewayService _paymentGateway;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ILogger<PlaceOrderCommandHandler> _logger;
|
||||||
|
|
||||||
public PlaceOrderCommandHandler(
|
public PlaceOrderCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IInventoryService inventoryService)
|
IInventoryService inventoryService,
|
||||||
|
IPaymentGatewayService paymentGateway,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<PlaceOrderCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_inventoryService = inventoryService;
|
_inventoryService = inventoryService;
|
||||||
|
_paymentGateway = paymentGateway;
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PlaceOrderResponseDto> Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
|
public async Task<PlaceOrderResponseDto> Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
|
||||||
@@ -98,19 +109,13 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate discount balance usage
|
// Always apply maximum possible discount (100%) — user cannot choose less
|
||||||
var maxDiscountBalanceUsable = totalDiscountAmount;
|
var maxDiscountBalanceUsable = totalDiscountAmount;
|
||||||
var actualDiscountBalanceUsed = Math.Min(request.DiscountBalanceToUse, maxDiscountBalanceUsable);
|
var actualDiscountBalanceUsed = Math.Min(maxDiscountBalanceUsable, userWallet.DiscountBalance);
|
||||||
actualDiscountBalanceUsed = Math.Min(actualDiscountBalanceUsed, userWallet.DiscountBalance);
|
|
||||||
|
|
||||||
if (actualDiscountBalanceUsed < request.DiscountBalanceToUse)
|
_logger.LogInformation(
|
||||||
{
|
"Discount auto-applied: max allowed={MaxAllowed}, wallet balance={WalletBalance}, used={Used}",
|
||||||
return new PlaceOrderResponseDto
|
maxDiscountBalanceUsable, userWallet.DiscountBalance, actualDiscountBalanceUsed);
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = $"موجودی تخفیف کافی نیست. حداکثر قابل استفاده: {maxDiscountBalanceUsable:N0} تومان، موجودی شما: {userWallet.DiscountBalance:N0} تومان"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var gatewayAmountRequired = totalAmount - actualDiscountBalanceUsed;
|
var gatewayAmountRequired = totalAmount - actualDiscountBalanceUsed;
|
||||||
|
|
||||||
@@ -172,15 +177,121 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
|||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// اگر مبلغ درگاه > ۰ باشد، باید به درگاه پرداخت متصل شویم
|
||||||
|
string? paymentUrl = null;
|
||||||
|
|
||||||
|
if (finalGatewayAmount > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// آدرس callback — زرینپال بعد از پرداخت کاربر را به اینجا هدایت میکند
|
||||||
|
var cmsBaseUrl = _configuration["CmsBaseUrl"] ?? "https://localhost:32846";
|
||||||
|
var callbackUrl = $"{cmsBaseUrl}/api/payment/discount-order/callback?orderId={order.Id}";
|
||||||
|
|
||||||
|
// درخواست به درگاه
|
||||||
|
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
|
||||||
|
{
|
||||||
|
Amount = finalGatewayAmount,
|
||||||
|
UserId = request.UserId,
|
||||||
|
Description = $"فروشگاه تخفیفی - سفارش #{order.Id}",
|
||||||
|
CallbackUrl = callbackUrl
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
if (paymentResult.IsSuccess && !string.IsNullOrEmpty(paymentResult.GatewayUrl))
|
||||||
|
{
|
||||||
|
// ذخیره Authority/RefId در تراکنش برای verify بعدی
|
||||||
|
transaction.RefId = paymentResult.RefId;
|
||||||
|
|
||||||
|
// ثبت PaymentTransaction — جدول جدید با اطلاعات درگاه
|
||||||
|
var paymentTx = new PaymentTransaction
|
||||||
|
{
|
||||||
|
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
|
||||||
|
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
|
||||||
|
Amount = finalGatewayAmount,
|
||||||
|
CallbackUrl = callbackUrl,
|
||||||
|
Description = $"فروشگاه تخفیفی - سفارش #{order.Id}",
|
||||||
|
Mobile = null,
|
||||||
|
UserId = request.UserId,
|
||||||
|
RequestStatusCode = 100,
|
||||||
|
RequestStatusMessage = "Success",
|
||||||
|
Authority = paymentResult.RefId,
|
||||||
|
PaymentStatus = false, // هنوز verify نشده
|
||||||
|
TransactionId = transaction.Id,
|
||||||
|
OrderId = order.Id.ToString()
|
||||||
|
};
|
||||||
|
_context.PaymentTransactions.Add(paymentTx);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
paymentUrl = paymentResult.GatewayUrl;
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Payment gateway initiated for DiscountOrder #{OrderId}: RefId={RefId}, Url={Url}",
|
||||||
|
order.Id, paymentResult.RefId, paymentResult.GatewayUrl);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Payment gateway initiation failed for DiscountOrder #{OrderId}: {Error}",
|
||||||
|
order.Id, paymentResult.ErrorMessage);
|
||||||
|
|
||||||
|
return new PlaceOrderResponseDto
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"خطا در اتصال به درگاه پرداخت: {paymentResult.ErrorMessage}",
|
||||||
|
OrderId = order.Id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Payment gateway exception for DiscountOrder #{OrderId}", order.Id);
|
||||||
|
return new PlaceOrderResponseDto
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"خطا در اتصال به درگاه پرداخت: {ex.Message}",
|
||||||
|
OrderId = order.Id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// اگر کل مبلغ از کیف تخفیفی پرداخت شد — مستقیماً تکمیل شود
|
||||||
|
transaction.PaymentStatus = PaymentStatus.Success;
|
||||||
|
transaction.PaymentDate = DateTime.Now;
|
||||||
|
order.PaymentStatus = PaymentStatus.Success;
|
||||||
|
order.PaymentDate = DateTime.Now;
|
||||||
|
order.DeliveryStatus = DeliveryStatus.Pending;
|
||||||
|
|
||||||
|
var walletForDeduct = await _context.UserWallets
|
||||||
|
.FirstOrDefaultAsync(w => w.UserId == request.UserId, cancellationToken);
|
||||||
|
if (walletForDeduct != null)
|
||||||
|
walletForDeduct.DiscountBalance -= actualDiscountBalanceUsed;
|
||||||
|
|
||||||
|
foreach (var cartItem in cartItems)
|
||||||
|
{
|
||||||
|
await _inventoryService.ConfirmSaleAsync(
|
||||||
|
cartItem.ProductId, ProductType.DiscountProduct,
|
||||||
|
cartItem.Count, order.Id, cancellationToken);
|
||||||
|
cartItem.Product.SaleCount += cartItem.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
_logger.LogInformation(
|
||||||
|
"DiscountOrder #{OrderId} fully paid via discount balance ({Amount} T)",
|
||||||
|
order.Id, actualDiscountBalanceUsed);
|
||||||
|
}
|
||||||
|
|
||||||
return new PlaceOrderResponseDto
|
return new PlaceOrderResponseDto
|
||||||
{
|
{
|
||||||
Success = true,
|
Success = true,
|
||||||
Message = "سفارش ایجاد شد. لطفا پرداخت را تکمیل کنید",
|
Message = finalGatewayAmount > 0
|
||||||
|
? "سفارش ایجاد شد. در حال انتقال به درگاه پرداخت..."
|
||||||
|
: "سفارش با موفقیت ثبت و پرداخت شد",
|
||||||
OrderId = order.Id,
|
OrderId = order.Id,
|
||||||
TransactionId = transaction.Id,
|
TransactionId = transaction.Id,
|
||||||
TotalAmount = totalAmount,
|
TotalAmount = totalAmount,
|
||||||
DiscountBalanceUsed = actualDiscountBalanceUsed,
|
DiscountBalanceUsed = actualDiscountBalanceUsed,
|
||||||
GatewayAmountRequired = finalGatewayAmount
|
GatewayAmountRequired = finalGatewayAmount,
|
||||||
|
PaymentUrl = paymentUrl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -12,7 +12,16 @@ public class UpdateDiscountProductCommand : IRequest<Unit>
|
|||||||
public int MaxDiscountPercent { get; set; }
|
public int MaxDiscountPercent { get; set; }
|
||||||
public string ImagePath { get; set; }
|
public string ImagePath { get; set; }
|
||||||
public string ThumbnailPath { get; set; }
|
public string ThumbnailPath { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
public int RemainingCount { get; set; }
|
public int RemainingCount { get; set; }
|
||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
public List<long> CategoryIds { get; set; } = new();
|
public List<long> CategoryIds { get; set; } = new();
|
||||||
|
|
||||||
|
// Image file upload
|
||||||
|
public byte[]? ImageFileBytes { get; set; }
|
||||||
|
public string? ImageFileMime { get; set; }
|
||||||
|
public string? ImageFileName { get; set; }
|
||||||
|
public byte[]? ThumbnailFileBytes { get; set; }
|
||||||
|
public string? ThumbnailFileMime { get; set; }
|
||||||
|
public string? ThumbnailFileName { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-3
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -8,10 +9,12 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateDiscountProd
|
|||||||
public class UpdateDiscountProductCommandHandler : IRequestHandler<UpdateDiscountProductCommand, Unit>
|
public class UpdateDiscountProductCommandHandler : IRequestHandler<UpdateDiscountProductCommand, Unit>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IFileManager _fileManager;
|
||||||
|
|
||||||
public UpdateDiscountProductCommandHandler(IApplicationDbContext context)
|
public UpdateDiscountProductCommandHandler(IApplicationDbContext context, IFileManager fileManager)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_fileManager = fileManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Unit> Handle(UpdateDiscountProductCommand request, CancellationToken cancellationToken)
|
public async Task<Unit> Handle(UpdateDiscountProductCommand request, CancellationToken cancellationToken)
|
||||||
@@ -27,11 +30,44 @@ public class UpdateDiscountProductCommandHandler : IRequestHandler<UpdateDiscoun
|
|||||||
product.FullInformation = request.FullInformation;
|
product.FullInformation = request.FullInformation;
|
||||||
product.Price = request.Price;
|
product.Price = request.Price;
|
||||||
product.MaxDiscountPercent = request.MaxDiscountPercent;
|
product.MaxDiscountPercent = request.MaxDiscountPercent;
|
||||||
product.ImagePath = request.ImagePath;
|
|
||||||
product.ThumbnailPath = request.ThumbnailPath;
|
|
||||||
product.RemainingCount = request.RemainingCount;
|
product.RemainingCount = request.RemainingCount;
|
||||||
product.IsActive = request.IsActive;
|
product.IsActive = request.IsActive;
|
||||||
|
|
||||||
|
// آپلود تصویر اصلی (اگر فایل جدید ارسال شده باشد)
|
||||||
|
if (request.ImageFileBytes is { Length: > 0 })
|
||||||
|
{
|
||||||
|
var result = await _fileManager.UploadImageAsync(
|
||||||
|
"Images/DiscountProducts",
|
||||||
|
request.ImageFileBytes,
|
||||||
|
request.ImageFileMime ?? "image/jpeg",
|
||||||
|
request.ImageFileName,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
product.ImagePath = result.Main.Path;
|
||||||
|
product.ThumbnailPath = result.Thumbnail.Path;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(request.ImagePath))
|
||||||
|
{
|
||||||
|
product.ImagePath = request.ImagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// آپلود بندانگشتی جداگانه (اختیاری)
|
||||||
|
if (request.ThumbnailFileBytes is { Length: > 0 })
|
||||||
|
{
|
||||||
|
var thumbResult = await _fileManager.UploadAsync(
|
||||||
|
"Images/DiscountProducts/Thumbnails",
|
||||||
|
request.ThumbnailFileBytes,
|
||||||
|
request.ThumbnailFileMime ?? "image/jpeg",
|
||||||
|
request.ThumbnailFileName,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
product.ThumbnailPath = thumbResult.Path;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(request.ThumbnailPath))
|
||||||
|
{
|
||||||
|
product.ThumbnailPath = request.ThumbnailPath;
|
||||||
|
}
|
||||||
|
|
||||||
// Update categories
|
// Update categories
|
||||||
var existingCategories = await _context.DiscountProductCategories
|
var existingCategories = await _context.DiscountProductCategories
|
||||||
.Where(pc => pc.ProductId == request.ProductId)
|
.Where(pc => pc.ProductId == request.ProductId)
|
||||||
|
|||||||
+2
@@ -43,4 +43,6 @@ public class OrderItemDto
|
|||||||
public int DiscountPercentUsed { get; set; }
|
public int DiscountPercentUsed { get; set; }
|
||||||
public long DiscountAmount { get; set; }
|
public long DiscountAmount { get; set; }
|
||||||
public long FinalPrice { get; set; }
|
public long FinalPrice { get; set; }
|
||||||
|
public string ImagePath { get; set; }
|
||||||
|
public string ThumbnailPath { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -53,7 +53,9 @@ public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, Order
|
|||||||
UnitPrice = od.UnitPrice,
|
UnitPrice = od.UnitPrice,
|
||||||
DiscountPercentUsed = od.DiscountPercentUsed,
|
DiscountPercentUsed = od.DiscountPercentUsed,
|
||||||
DiscountAmount = od.DiscountAmount,
|
DiscountAmount = od.DiscountAmount,
|
||||||
FinalPrice = od.FinalPrice
|
FinalPrice = od.FinalPrice,
|
||||||
|
ImagePath = od.Product.ImagePath ?? "",
|
||||||
|
ThumbnailPath = od.Product.ThumbnailPath ?? ""
|
||||||
}).ToList()
|
}).ToList()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStat
|
|||||||
public class GetNetworkStatisticsQuery : IRequest<GetNetworkStatisticsResponseDto>
|
public class GetNetworkStatisticsQuery : IRequest<GetNetworkStatisticsResponseDto>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// شناسه کاربر برای محاسبه آمار شبکه او - 0 یا null یعنی کاربر جاری
|
/// شناسه کاربر برای محاسبه آمار شبکه او - 0 یعنی آمار کل شبکه (root user)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long UserId { get; set; }
|
public long UserId { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-12
@@ -5,30 +5,33 @@ namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStat
|
|||||||
public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatisticsQuery, GetNetworkStatisticsResponseDto>
|
public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatisticsQuery, GetNetworkStatisticsResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly ICurrentUserService _currentUser;
|
|
||||||
|
|
||||||
public GetNetworkStatisticsQueryHandler(
|
public GetNetworkStatisticsQueryHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context)
|
||||||
ICurrentUserService currentUser)
|
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_currentUser = currentUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetNetworkStatisticsResponseDto> Handle(GetNetworkStatisticsQuery request, CancellationToken cancellationToken)
|
public async Task<GetNetworkStatisticsResponseDto> Handle(GetNetworkStatisticsQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Get userId - use current user if not specified or is 0
|
// UserId > 0 → stats for that user's network
|
||||||
var userId = request.UserId == 0
|
// UserId == 0 → global stats (find root user of the network)
|
||||||
? (long.TryParse(_currentUser.UserId, out var currentUserId) ? currentUserId : 0)
|
var userId = request.UserId;
|
||||||
: request.UserId;
|
|
||||||
|
// Get all users (needed for descendant calculation anyway)
|
||||||
|
var allUsers = await _context.Users.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
if (userId == 0)
|
if (userId == 0)
|
||||||
{
|
{
|
||||||
throw new UnauthorizedAccessException("User ID not found");
|
// Find root user (user with no NetworkParentId)
|
||||||
|
var rootUser = allUsers.FirstOrDefault(x => x.NetworkParentId == null || x.NetworkParentId == 0);
|
||||||
|
if (rootUser != null)
|
||||||
|
userId = rootUser.Id;
|
||||||
|
else if (allUsers.Count > 0)
|
||||||
|
userId = allUsers.First().Id;
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException("No users found in the system");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all descendants recursively
|
|
||||||
var allUsers = await _context.Users.ToListAsync(cancellationToken);
|
|
||||||
var allDescendants = GetAllDescendants(userId, allUsers);
|
var allDescendants = GetAllDescendants(userId, allUsers);
|
||||||
|
|
||||||
// Statistics for the user's network (all descendants)
|
// Statistics for the user's network (all descendants)
|
||||||
|
|||||||
+4
-9
@@ -8,28 +8,23 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly ILogger<GetNetworkTreeQueryHandler> _logger;
|
private readonly ILogger<GetNetworkTreeQueryHandler> _logger;
|
||||||
private readonly ICurrentUserService _currentUser;
|
|
||||||
|
|
||||||
public GetNetworkTreeQueryHandler(
|
public GetNetworkTreeQueryHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
ILogger<GetNetworkTreeQueryHandler> logger,
|
ILogger<GetNetworkTreeQueryHandler> logger)
|
||||||
ICurrentUserService currentUser)
|
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_currentUser = currentUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<NetworkTreeDto?> Handle(GetNetworkTreeQuery request, CancellationToken cancellationToken)
|
public async Task<NetworkTreeDto?> Handle(GetNetworkTreeQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Get userId - use current user if UserId is 0
|
// UserId must be provided - customer endpoints resolve from JWT before calling
|
||||||
var userId = request.UserId == 0
|
var userId = request.UserId;
|
||||||
? (long.TryParse(_currentUser.UserId, out var currentUserId) ? currentUserId : 0)
|
|
||||||
: request.UserId;
|
|
||||||
|
|
||||||
if (userId == 0)
|
if (userId == 0)
|
||||||
{
|
{
|
||||||
throw new UnauthorizedAccessException("User ID not found");
|
throw new ArgumentException("UserId is required for network tree query");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new request with the resolved userId
|
// Create a new request with the resolved userId
|
||||||
|
|||||||
+4
-1
@@ -5,5 +5,8 @@ public record CreateNewOtpTokenCommand : IRequest<CreateNewOtpTokenResponseDto>
|
|||||||
public string Mobile { get; init; }
|
public string Mobile { get; init; }
|
||||||
//مقصود
|
//مقصود
|
||||||
public string Purpose { get; init; }
|
public string Purpose { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه GUID قرارداد (فقط برای purpose=signcontract/signclubcontract)
|
||||||
|
/// </summary>
|
||||||
|
public string? SignGuid { get; init; }
|
||||||
}
|
}
|
||||||
+1
-1
@@ -57,7 +57,7 @@ public class CreateNewOtpTokenCommandHandler : IRequestHandler<CreateNewOtpToken
|
|||||||
|
|
||||||
};
|
};
|
||||||
await _context.OtpTokens.AddAsync(entity, cancellationToken);
|
await _context.OtpTokens.AddAsync(entity, cancellationToken);
|
||||||
entity.AddDomainEvent(new CreateNewOtpTokenEvent(entity, code));
|
entity.AddDomainEvent(new CreateNewOtpTokenEvent(entity, code, request.SignGuid));
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
return new CreateNewOtpTokenResponseDto()
|
return new CreateNewOtpTokenResponseDto()
|
||||||
{
|
{
|
||||||
|
|||||||
+13
@@ -23,8 +23,21 @@ public class CreateNewOtpTokenEventHandler : INotificationHandler<CreateNewOtpTo
|
|||||||
notification.GetType().Name, notification.Item.Mobile);
|
notification.GetType().Name, notification.Item.Mobile);
|
||||||
|
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
var purpose = notification.Item.Purpose?.ToLowerInvariant();
|
||||||
|
|
||||||
|
// برای امضای قرارداد، پیامک ساده با GUID ارسال شود
|
||||||
|
if ((purpose == "signcontract" || purpose == "signclubcontract")
|
||||||
|
&& !string.IsNullOrEmpty(notification.SignGuid))
|
||||||
|
{
|
||||||
|
var message = $"کد تایید امضای قرارداد: {notification.PlainCode}\nشناسه قرارداد: {notification.SignGuid}";
|
||||||
|
await _kavenegarService.SendAsync(notification.Item.Mobile, message);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
await _kavenegarService.VerifyLookupAsync(notification.Item.Mobile, notification.PlainCode);
|
await _kavenegarService.VerifyLookupAsync(notification.Item.Mobile, notification.PlainCode);
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("OTP SMS sent successfully to {Mobile}", notification.Item.Mobile);
|
_logger.LogInformation("OTP SMS sent successfully to {Mobile}", notification.Item.Mobile);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
+5
-1
@@ -5,6 +5,7 @@ using CMSMicroservice.Domain.Entities;
|
|||||||
using CMSMicroservice.Domain.Enums;
|
using CMSMicroservice.Domain.Enums;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ValidationException = FluentValidation.ValidationException;
|
using ValidationException = FluentValidation.ValidationException;
|
||||||
|
|
||||||
@@ -15,15 +16,18 @@ public class PurchasePackageCommandHandler
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IPaymentGatewayService _paymentGateway;
|
private readonly IPaymentGatewayService _paymentGateway;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<PurchasePackageCommandHandler> _logger;
|
private readonly ILogger<PurchasePackageCommandHandler> _logger;
|
||||||
|
|
||||||
public PurchasePackageCommandHandler(
|
public PurchasePackageCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IPaymentGatewayService paymentGateway,
|
IPaymentGatewayService paymentGateway,
|
||||||
|
IConfiguration configuration,
|
||||||
ILogger<PurchasePackageCommandHandler> logger)
|
ILogger<PurchasePackageCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_paymentGateway = paymentGateway;
|
_paymentGateway = paymentGateway;
|
||||||
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +120,7 @@ public class PurchasePackageCommandHandler
|
|||||||
Amount = order.Amount,
|
Amount = order.Amount,
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
Mobile = user.Mobile ?? "",
|
Mobile = user.Mobile ?? "",
|
||||||
CallbackUrl = $"https://yourdomain.com/api/package/verify-package",
|
CallbackUrl = $"{_configuration["CmsBaseUrl"] ?? "https://localhost:32846"}/api/package/verify-package",
|
||||||
Description = $"خرید پکیج - سفارش #{order.Id}"
|
Description = $"خرید پکیج - سفارش #{order.Id}"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+14
-25
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -7,64 +8,52 @@ namespace CMSMicroservice.Application.ProductsCQ.Commands.AddProductImage;
|
|||||||
public class AddProductImageCommandHandler : IRequestHandler<AddProductImageCommand, AddProductImageResponseDto>
|
public class AddProductImageCommandHandler : IRequestHandler<AddProductImageCommand, AddProductImageResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IFileManagementService _fileManagementService;
|
private readonly IFileManager _fileManager;
|
||||||
private readonly ILogger<AddProductImageCommandHandler> _logger;
|
private readonly ILogger<AddProductImageCommandHandler> _logger;
|
||||||
|
|
||||||
public AddProductImageCommandHandler(
|
public AddProductImageCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IFileManagementService fileManagementService,
|
IFileManager fileManager,
|
||||||
ILogger<AddProductImageCommandHandler> logger)
|
ILogger<AddProductImageCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_fileManagementService = fileManagementService;
|
_fileManager = fileManager;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AddProductImageResponseDto> Handle(AddProductImageCommand request,
|
public async Task<AddProductImageResponseDto> Handle(AddProductImageCommand request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Verify product exists
|
// بررسی وجود محصول
|
||||||
var productExists = await _context.Products
|
var productExists = await _context.Products
|
||||||
.AnyAsync(p => p.Id == request.ProductId, cancellationToken);
|
.AnyAsync(p => p.Id == request.ProductId, cancellationToken);
|
||||||
if (!productExists)
|
if (!productExists)
|
||||||
throw new NotFoundException(nameof(Product), request.ProductId);
|
throw new NotFoundException(nameof(Product), request.ProductId);
|
||||||
|
|
||||||
string imagePath = string.Empty;
|
// بدون فایل تصویر، کاری انجام نمیشود
|
||||||
string thumbnailPath = string.Empty;
|
if (request.ImageFileBytes is not { Length: > 0 })
|
||||||
|
throw new FileUploadException("فایل تصویر ارسال نشده است");
|
||||||
|
|
||||||
// Upload image to FMS
|
// آپلود تصویر به FMS (اگر خطا بخوره، exception پرتاب میشه و entity ذخیره نمیشه)
|
||||||
if (request.ImageFileBytes is { Length: > 0 })
|
var uploaded = await _fileManager.UploadImageAsync(
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var (mainPath, thumbPath) = await _fileManagementService.UploadImageWithThumbnailAsync(
|
|
||||||
"Images/Products/Gallery",
|
"Images/Products/Gallery",
|
||||||
request.ImageFileBytes,
|
request.ImageFileBytes,
|
||||||
request.ImageFileMime ?? "image/jpeg",
|
request.ImageFileMime ?? "image/jpeg",
|
||||||
request.ImageFileName,
|
request.ImageFileName,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
imagePath = mainPath ?? string.Empty;
|
// ساخت رکورد تصویر
|
||||||
thumbnailPath = thumbPath ?? string.Empty;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to upload gallery image to FMS for product {ProductId}", request.ProductId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create ProductImage entity
|
|
||||||
var productImage = new ProductImage
|
var productImage = new ProductImage
|
||||||
{
|
{
|
||||||
Title = request.Title,
|
Title = request.Title,
|
||||||
ImagePath = imagePath,
|
ImagePath = uploaded.Main.Path,
|
||||||
ImageThumbnailPath = thumbnailPath
|
ImageThumbnailPath = uploaded.Thumbnail.Path
|
||||||
};
|
};
|
||||||
|
|
||||||
await _context.ProductImages.AddAsync(productImage, cancellationToken);
|
await _context.ProductImages.AddAsync(productImage, cancellationToken);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// Create ProductGallery join entity
|
// اتصال تصویر به محصول
|
||||||
var productGallery = new ProductGallery
|
var productGallery = new ProductGallery
|
||||||
{
|
{
|
||||||
ProductId = request.ProductId,
|
ProductId = request.ProductId,
|
||||||
|
|||||||
+14
-31
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -7,16 +8,16 @@ namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
|||||||
public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProductsCommand, CreateNewProductsResponseDto>
|
public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProductsCommand, CreateNewProductsResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IFileManagementService _fileManagementService;
|
private readonly IFileManager _fileManager;
|
||||||
private readonly ILogger<CreateNewProductsCommandHandler> _logger;
|
private readonly ILogger<CreateNewProductsCommandHandler> _logger;
|
||||||
|
|
||||||
public CreateNewProductsCommandHandler(
|
public CreateNewProductsCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IFileManagementService fileManagementService,
|
IFileManager fileManager,
|
||||||
ILogger<CreateNewProductsCommandHandler> logger)
|
ILogger<CreateNewProductsCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_fileManagementService = fileManagementService;
|
_fileManager = fileManager;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,56 +33,38 @@ public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProducts
|
|||||||
Price = request.Price,
|
Price = request.Price,
|
||||||
Discount = request.Discount,
|
Discount = request.Discount,
|
||||||
Rate = request.Rate,
|
Rate = request.Rate,
|
||||||
ImagePath = request.ImagePath ?? string.Empty,
|
ImagePath = string.Empty,
|
||||||
ThumbnailPath = request.ThumbnailPath ?? string.Empty,
|
ThumbnailPath = string.Empty,
|
||||||
SaleCount = request.SaleCount,
|
SaleCount = request.SaleCount,
|
||||||
ViewCount = request.ViewCount,
|
ViewCount = request.ViewCount,
|
||||||
RemainingCount = request.RemainingCount
|
RemainingCount = request.RemainingCount
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle image upload to FMS if file bytes provided
|
// آپلود تصویر اصلی (اگر ارسال شده باشد)
|
||||||
if (request.ImageFileBytes is { Length: > 0 })
|
if (request.ImageFileBytes is { Length: > 0 })
|
||||||
{
|
{
|
||||||
try
|
var result = await _fileManager.UploadImageAsync(
|
||||||
{
|
|
||||||
var (mainPath, thumbPath) = await _fileManagementService.UploadImageWithThumbnailAsync(
|
|
||||||
"Images/Products",
|
"Images/Products",
|
||||||
request.ImageFileBytes,
|
request.ImageFileBytes,
|
||||||
request.ImageFileMime ?? "image/jpeg",
|
request.ImageFileMime ?? "image/jpeg",
|
||||||
request.ImageFileName,
|
request.ImageFileName,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(mainPath))
|
entity.ImagePath = result.Main.Path;
|
||||||
entity.ImagePath = mainPath;
|
entity.ThumbnailPath = result.Thumbnail.Path;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(thumbPath))
|
|
||||||
entity.ThumbnailPath = thumbPath;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to upload product image to FMS");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle separate thumbnail upload if provided (and not already set from main image)
|
// آپلود بندانگشتی جداگانه (اختیاری — جایگزین بندانگشتی خودکار)
|
||||||
if (request.ThumbnailFileBytes is { Length: > 0 } && string.IsNullOrWhiteSpace(entity.ThumbnailPath))
|
if (request.ThumbnailFileBytes is { Length: > 0 })
|
||||||
{
|
{
|
||||||
try
|
var thumbResult = await _fileManager.UploadAsync(
|
||||||
{
|
|
||||||
var thumbPath = await _fileManagementService.UploadFileAsync(
|
|
||||||
"Images/Products/Thumbnails",
|
"Images/Products/Thumbnails",
|
||||||
request.ThumbnailFileBytes,
|
request.ThumbnailFileBytes,
|
||||||
request.ThumbnailFileMime ?? "image/jpeg",
|
request.ThumbnailFileMime ?? "image/jpeg",
|
||||||
request.ThumbnailFileName,
|
request.ThumbnailFileName,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(thumbPath))
|
entity.ThumbnailPath = thumbResult.Path;
|
||||||
entity.ThumbnailPath = thumbPath;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to upload product thumbnail to FMS");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await _context.Products.AddAsync(entity, cancellationToken);
|
await _context.Products.AddAsync(entity, cancellationToken);
|
||||||
|
|||||||
+11
-36
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -7,16 +8,16 @@ namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
|||||||
public class UpdateProductsCommandHandler : IRequestHandler<UpdateProductsCommand, Unit>
|
public class UpdateProductsCommandHandler : IRequestHandler<UpdateProductsCommand, Unit>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IFileManagementService _fileManagementService;
|
private readonly IFileManager _fileManager;
|
||||||
private readonly ILogger<UpdateProductsCommandHandler> _logger;
|
private readonly ILogger<UpdateProductsCommandHandler> _logger;
|
||||||
|
|
||||||
public UpdateProductsCommandHandler(
|
public UpdateProductsCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IFileManagementService fileManagementService,
|
IFileManager fileManager,
|
||||||
ILogger<UpdateProductsCommandHandler> logger)
|
ILogger<UpdateProductsCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_fileManagementService = fileManagementService;
|
_fileManager = fileManager;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,57 +39,31 @@ public class UpdateProductsCommandHandler : IRequestHandler<UpdateProductsComman
|
|||||||
entity.ViewCount = request.ViewCount;
|
entity.ViewCount = request.ViewCount;
|
||||||
entity.RemainingCount = request.RemainingCount;
|
entity.RemainingCount = request.RemainingCount;
|
||||||
|
|
||||||
// Handle image upload to FMS if new file bytes provided
|
// آپلود تصویر اصلی جدید (اگر ارسال شده باشد)
|
||||||
if (request.ImageFileBytes is { Length: > 0 })
|
if (request.ImageFileBytes is { Length: > 0 })
|
||||||
{
|
{
|
||||||
try
|
var result = await _fileManager.UploadImageAsync(
|
||||||
{
|
|
||||||
var (mainPath, thumbPath) = await _fileManagementService.UploadImageWithThumbnailAsync(
|
|
||||||
"Images/Products",
|
"Images/Products",
|
||||||
request.ImageFileBytes,
|
request.ImageFileBytes,
|
||||||
request.ImageFileMime ?? "image/jpeg",
|
request.ImageFileMime ?? "image/jpeg",
|
||||||
request.ImageFileName,
|
request.ImageFileName,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(mainPath))
|
entity.ImagePath = result.Main.Path;
|
||||||
entity.ImagePath = mainPath;
|
entity.ThumbnailPath = result.Thumbnail.Path;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(thumbPath))
|
|
||||||
entity.ThumbnailPath = thumbPath;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to upload updated product image to FMS for product {ProductId}", request.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// If no new file uploaded, keep existing paths or update from request
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.ImagePath))
|
|
||||||
entity.ImagePath = request.ImagePath;
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.ThumbnailPath))
|
|
||||||
entity.ThumbnailPath = request.ThumbnailPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle separate thumbnail upload if provided
|
// آپلود بندانگشتی جداگانه (اختیاری — جایگزین بندانگشتی خودکار)
|
||||||
if (request.ThumbnailFileBytes is { Length: > 0 })
|
if (request.ThumbnailFileBytes is { Length: > 0 })
|
||||||
{
|
{
|
||||||
try
|
var thumbResult = await _fileManager.UploadAsync(
|
||||||
{
|
|
||||||
var thumbPath = await _fileManagementService.UploadFileAsync(
|
|
||||||
"Images/Products/Thumbnails",
|
"Images/Products/Thumbnails",
|
||||||
request.ThumbnailFileBytes,
|
request.ThumbnailFileBytes,
|
||||||
request.ThumbnailFileMime ?? "image/jpeg",
|
request.ThumbnailFileMime ?? "image/jpeg",
|
||||||
request.ThumbnailFileName,
|
request.ThumbnailFileName,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(thumbPath))
|
entity.ThumbnailPath = thumbResult.Path;
|
||||||
entity.ThumbnailPath = thumbPath;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to upload updated product thumbnail to FMS for product {ProductId}", request.Id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_context.Products.Update(entity);
|
_context.Products.Update(entity);
|
||||||
|
|||||||
+1
@@ -21,5 +21,6 @@ public class GetCustomerProductsByFilterQuery : IRequest<GetCustomerProductsByFi
|
|||||||
public int? SaleCount { get; set; }
|
public int? SaleCount { get; set; }
|
||||||
public int? ViewCount { get; set; }
|
public int? ViewCount { get; set; }
|
||||||
public int? RemainingCount { get; set; }
|
public int? RemainingCount { get; set; }
|
||||||
|
public bool? IsActive { get; set; }
|
||||||
public List<long>? CategoryIds { get; set; }
|
public List<long>? CategoryIds { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -59,6 +59,9 @@ public class GetCustomerProductsByFilterQueryHandler : IRequestHandler<GetCustom
|
|||||||
if (request.CategoryIds != null && request.CategoryIds.Any())
|
if (request.CategoryIds != null && request.CategoryIds.Any())
|
||||||
query = query.Where(x => x.ProductCategories.Any(pc => request.CategoryIds.Contains(pc.CategoryId)));
|
query = query.Where(x => x.ProductCategories.Any(pc => request.CategoryIds.Contains(pc.CategoryId)));
|
||||||
|
|
||||||
|
if (request.IsActive.HasValue)
|
||||||
|
query = query.Where(x => x.IsDeleted != request.IsActive.Value);
|
||||||
|
|
||||||
// Apply sorting
|
// Apply sorting
|
||||||
if (!string.IsNullOrEmpty(request.SortBy))
|
if (!string.IsNullOrEmpty(request.SortBy))
|
||||||
query = query.ApplyOrder(request.SortBy);
|
query = query.ApplyOrder(request.SortBy);
|
||||||
@@ -99,6 +102,7 @@ public class GetCustomerProductsByFilterQueryHandler : IRequestHandler<GetCustom
|
|||||||
SaleCount = p.SaleCount,
|
SaleCount = p.SaleCount,
|
||||||
ViewCount = p.ViewCount,
|
ViewCount = p.ViewCount,
|
||||||
RemainingCount = p.RemainingCount,
|
RemainingCount = p.RemainingCount,
|
||||||
|
IsActive = !p.IsDeleted,
|
||||||
Categories = p.ProductCategories?.Select(pc => new ProductCategoryPathModel
|
Categories = p.ProductCategories?.Select(pc => new ProductCategoryPathModel
|
||||||
{
|
{
|
||||||
CategoryId = pc.CategoryId,
|
CategoryId = pc.CategoryId,
|
||||||
|
|||||||
+1
@@ -23,6 +23,7 @@ public class CustomerProductModel
|
|||||||
public int SaleCount { get; set; }
|
public int SaleCount { get; set; }
|
||||||
public int ViewCount { get; set; }
|
public int ViewCount { get; set; }
|
||||||
public int RemainingCount { get; set; }
|
public int RemainingCount { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
public List<ProductCategoryPathModel> Categories { get; set; }
|
public List<ProductCategoryPathModel> Categories { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.SitePageCQ.Commands.CreateSitePage;
|
||||||
|
|
||||||
|
public class CreateSitePageCommand : IRequest<long>
|
||||||
|
{
|
||||||
|
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; } = true;
|
||||||
|
|
||||||
|
// Image upload properties
|
||||||
|
public byte[]? ImageFileBytes { get; set; }
|
||||||
|
public string? ImageFileMime { get; set; }
|
||||||
|
public string? ImageFileName { get; set; }
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities.Content;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.SitePageCQ.Commands.CreateSitePage;
|
||||||
|
|
||||||
|
public class CreateSitePageCommandHandler : IRequestHandler<CreateSitePageCommand, long>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IFileManager _fileManager;
|
||||||
|
|
||||||
|
public CreateSitePageCommandHandler(IApplicationDbContext context, IFileManager fileManager)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_fileManager = fileManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> Handle(CreateSitePageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = new SitePage
|
||||||
|
{
|
||||||
|
PageKey = request.PageKey,
|
||||||
|
Title = request.Title,
|
||||||
|
MetaDescription = request.MetaDescription,
|
||||||
|
HeroTitle = request.HeroTitle,
|
||||||
|
HeroSubtitle = request.HeroSubtitle,
|
||||||
|
IsActive = request.IsActive
|
||||||
|
};
|
||||||
|
|
||||||
|
// آپلود تصویر هیرو (اگر فایل ارسال شده باشد)
|
||||||
|
if (request.ImageFileBytes is { Length: > 0 })
|
||||||
|
{
|
||||||
|
var result = await _fileManager.UploadImageAsync(
|
||||||
|
"Images/SitePages",
|
||||||
|
request.ImageFileBytes,
|
||||||
|
request.ImageFileMime ?? "image/jpeg",
|
||||||
|
request.ImageFileName,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
entity.HeroImagePath = result.Main.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.SitePages.Add(entity);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return entity.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.SitePageCQ.Commands.CreateSitePageSection;
|
||||||
|
|
||||||
|
public class CreateSitePageSectionCommand : IRequest<long>
|
||||||
|
{
|
||||||
|
public long SitePageId { get; set; }
|
||||||
|
public string SectionKey { get; set; } = default!;
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? Subtitle { get; set; }
|
||||||
|
public string? HtmlContent { get; set; }
|
||||||
|
public string? IconName { get; set; }
|
||||||
|
public string? ImagePath { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
public string? ExtraData { get; set; }
|
||||||
|
|
||||||
|
// Image upload properties
|
||||||
|
public byte[]? ImageFileBytes { get; set; }
|
||||||
|
public string? ImageFileMime { get; set; }
|
||||||
|
public string? ImageFileName { get; set; }
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities.Content;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.SitePageCQ.Commands.CreateSitePageSection;
|
||||||
|
|
||||||
|
public class CreateSitePageSectionCommandHandler : IRequestHandler<CreateSitePageSectionCommand, long>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IFileManager _fileManager;
|
||||||
|
|
||||||
|
public CreateSitePageSectionCommandHandler(IApplicationDbContext context, IFileManager fileManager)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_fileManager = fileManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> Handle(CreateSitePageSectionCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var page = await _context.SitePages.FirstOrDefaultAsync(x => x.Id == request.SitePageId && !x.IsDeleted, cancellationToken);
|
||||||
|
if (page == null)
|
||||||
|
throw new NotFoundException(nameof(SitePage), request.SitePageId);
|
||||||
|
|
||||||
|
var maxSortOrder = await _context.SitePageSections
|
||||||
|
.Where(x => x.SitePageId == request.SitePageId && !x.IsDeleted)
|
||||||
|
.MaxAsync(x => (int?)x.SortOrder, cancellationToken) ?? 0;
|
||||||
|
|
||||||
|
var sectionKey = string.IsNullOrWhiteSpace(request.SectionKey)
|
||||||
|
? $"section-{Guid.NewGuid():N}"[..20]
|
||||||
|
: request.SectionKey.Trim().ToLower();
|
||||||
|
|
||||||
|
var entity = new SitePageSection
|
||||||
|
{
|
||||||
|
SitePageId = request.SitePageId,
|
||||||
|
SectionKey = sectionKey,
|
||||||
|
Title = request.Title,
|
||||||
|
Subtitle = request.Subtitle,
|
||||||
|
HtmlContent = request.HtmlContent,
|
||||||
|
IconName = request.IconName,
|
||||||
|
ImagePath = request.ImagePath,
|
||||||
|
SortOrder = maxSortOrder + 1,
|
||||||
|
IsActive = request.IsActive,
|
||||||
|
ExtraData = request.ExtraData
|
||||||
|
};
|
||||||
|
|
||||||
|
// آپلود تصویر بخش (اگر فایل ارسال شده باشد)
|
||||||
|
if (request.ImageFileBytes is { Length: > 0 })
|
||||||
|
{
|
||||||
|
var result = await _fileManager.UploadImageAsync(
|
||||||
|
"Images/SitePageSections",
|
||||||
|
request.ImageFileBytes,
|
||||||
|
request.ImageFileMime ?? "image/jpeg",
|
||||||
|
request.ImageFileName,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
entity.ImagePath = result.Main.Path;
|
||||||
|
entity.ImageThumbnailPath = result.Thumbnail.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.SitePageSections.Add(entity);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return entity.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.SitePageCQ.Commands.CreateSitePageSection;
|
||||||
|
|
||||||
|
public class CreateSitePageSectionCommandValidator : AbstractValidator<CreateSitePageSectionCommand>
|
||||||
|
{
|
||||||
|
public CreateSitePageSectionCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.SitePageId)
|
||||||
|
.GreaterThan(0).WithMessage("شناسه صفحه نامعتبر است");
|
||||||
|
|
||||||
|
RuleFor(x => x.SectionKey)
|
||||||
|
.MaximumLength(100).WithMessage("کلید بخش حداکثر ۱۰۰ کاراکتر")
|
||||||
|
.Matches(@"^[a-z0-9\-_]*$").WithMessage("کلید بخش فقط شامل حروف کوچک، اعداد، خط تیره و زیرخط")
|
||||||
|
.When(x => !string.IsNullOrWhiteSpace(x.SectionKey));
|
||||||
|
|
||||||
|
RuleFor(x => x.Title)
|
||||||
|
.MaximumLength(300).WithMessage("عنوان بخش حداکثر ۳۰۰ کاراکتر");
|
||||||
|
|
||||||
|
RuleFor(x => x.Subtitle)
|
||||||
|
.MaximumLength(500).WithMessage("زیرعنوان بخش حداکثر ۵۰۰ کاراکتر");
|
||||||
|
|
||||||
|
RuleFor(x => x.IconName)
|
||||||
|
.MaximumLength(100).WithMessage("نام آیکون حداکثر ۱۰۰ کاراکتر");
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.SitePageCQ.Commands.DeleteSitePage;
|
||||||
|
|
||||||
|
public class DeleteSitePageCommand : IRequest<Unit>
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities.Content;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.SitePageCQ.Commands.DeleteSitePage;
|
||||||
|
|
||||||
|
public class DeleteSitePageCommandHandler : IRequestHandler<DeleteSitePageCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public DeleteSitePageCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(DeleteSitePageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await _context.SitePages.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||||
|
if (entity == null || entity.IsDeleted)
|
||||||
|
throw new NotFoundException(nameof(SitePage), request.Id);
|
||||||
|
|
||||||
|
entity.IsDeleted = true;
|
||||||
|
|
||||||
|
// حذف نرم بخشهای وابسته
|
||||||
|
foreach (var section in entity.Sections.Where(s => !s.IsDeleted))
|
||||||
|
{
|
||||||
|
section.IsDeleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.SitePageCQ.Commands.DeleteSitePageSection;
|
||||||
|
|
||||||
|
public class DeleteSitePageSectionCommand : 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.Content;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.SitePageCQ.Commands.DeleteSitePageSection;
|
||||||
|
|
||||||
|
public class DeleteSitePageSectionCommandHandler : IRequestHandler<DeleteSitePageSectionCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public DeleteSitePageSectionCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(DeleteSitePageSectionCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await _context.SitePageSections.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||||
|
if (entity == null || entity.IsDeleted)
|
||||||
|
throw new NotFoundException(nameof(SitePageSection), request.Id);
|
||||||
|
|
||||||
|
entity.IsDeleted = true;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.SitePageCQ.Commands.ReorderSitePageSections;
|
||||||
|
|
||||||
|
public class ReorderSitePageSectionsCommand : IRequest<Unit>
|
||||||
|
{
|
||||||
|
public List<SectionSortItem> Items { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SectionSortItem
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.SitePageCQ.Commands.ReorderSitePageSections;
|
||||||
|
|
||||||
|
public class ReorderSitePageSectionsCommandHandler : IRequestHandler<ReorderSitePageSectionsCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public ReorderSitePageSectionsCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(ReorderSitePageSectionsCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var ids = request.Items.Select(x => x.Id).ToList();
|
||||||
|
var sections = await _context.SitePageSections
|
||||||
|
.Where(x => ids.Contains(x.Id) && !x.IsDeleted)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
foreach (var item in request.Items)
|
||||||
|
{
|
||||||
|
var section = sections.FirstOrDefault(x => x.Id == item.Id);
|
||||||
|
if (section != null)
|
||||||
|
section.SortOrder = item.SortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user