Add migration for DiscountProductImages table
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m53s

- Created a new table `DiscountProductImages` in the CMS schema.
- Added columns for image details including `Title`, `AltText`, `ImagePath`, `ThumbnailPath`, `SortOrder`, `IsActive`, `Created`, `CreatedBy`, `LastModified`, `LastModifiedBy`, and `IsDeleted`.
- Established a foreign key relationship with the `DiscountProducts` table.
- Created indexes on `DiscountProductId` and a composite index on `DiscountProductId` and `SortOrder`.
This commit is contained in:
masoodafar-web
2026-01-01 02:26:33 +03:30
parent 500e169141
commit f0117eb1d5
29 changed files with 5068 additions and 4 deletions
@@ -0,0 +1,102 @@
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Enums;
using MediatR;
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetAllDiscountOrders;
/// <summary>
/// کوئری دریافت همه سفارشات فروشگاه تخفیفی برای ادمین
/// </summary>
public class GetAllDiscountOrdersQuery : IRequest<GetAllDiscountOrdersResponseDto>
{
public PaginationState? PaginationQuery { get; set; }
/// <summary>
/// فیلتر بر اساس شناسه کاربر
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// فیلتر بر اساس وضعیت پرداخت
/// </summary>
public PaymentStatus? PaymentStatus { get; set; }
/// <summary>
/// فیلتر بر اساس وضعیت ارسال
/// </summary>
public DeliveryStatus? DeliveryStatus { get; set; }
/// <summary>
/// جستجو بر اساس موبایل کاربر
/// </summary>
public string? UserMobile { get; set; }
/// <summary>
/// جستجو بر اساس کد رهگیری
/// </summary>
public string? TrackingCode { get; set; }
/// <summary>
/// فیلتر از تاریخ
/// </summary>
public DateTime? FromDate { get; set; }
/// <summary>
/// فیلتر تا تاریخ
/// </summary>
public DateTime? ToDate { get; set; }
/// <summary>
/// حداقل مبلغ سفارش
/// </summary>
public long? MinAmount { get; set; }
/// <summary>
/// حداکثر مبلغ سفارش
/// </summary>
public long? MaxAmount { get; set; }
}
public class GetAllDiscountOrdersResponseDto
{
public MetaData MetaData { get; set; } = new();
public List<AdminOrderDto> Models { get; set; } = new();
}
public class AdminOrderDto
{
public long Id { get; set; }
// اطلاعات کاربر
public long UserId { get; set; }
public string UserFullName { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
// مبالغ
public long TotalAmount { get; set; }
public long DiscountBalanceUsed { get; set; }
public long GatewayAmountPaid { get; set; }
public long VatAmount { get; set; }
// وضعیت ها
public PaymentStatus PaymentStatus { get; set; }
public DateTime? PaymentDate { get; set; }
public DeliveryStatus DeliveryStatus { get; set; }
public DateTime? DeliveryDate { get; set; }
// آدرس تحویل
public string? ShippingAddress { get; set; }
public string? ReceiverName { get; set; }
public string? ReceiverMobile { get; set; }
// رهگیری
public string? TrackingCode { get; set; }
public string? AdminNote { get; set; }
// تاریخ ها
public DateTime Created { get; set; }
public DateTime? LastModified { get; set; }
// تعداد آیتم ها
public int ItemsCount { get; set; }
}
@@ -0,0 +1,124 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetAllDiscountOrders;
public class GetAllDiscountOrdersQueryHandler : IRequestHandler<GetAllDiscountOrdersQuery, GetAllDiscountOrdersResponseDto>
{
private readonly IApplicationDbContext _context;
public GetAllDiscountOrdersQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetAllDiscountOrdersResponseDto> Handle(GetAllDiscountOrdersQuery request, CancellationToken cancellationToken)
{
var query = _context.DiscountOrders
.Include(o => o.User)
.Include(o => o.UserAddress)
.AsQueryable();
// فیلتر بر اساس شناسه کاربر
if (request.UserId.HasValue)
{
query = query.Where(o => o.UserId == request.UserId.Value);
}
// فیلتر بر اساس وضعیت پرداخت
if (request.PaymentStatus.HasValue)
{
query = query.Where(o => o.PaymentStatus == request.PaymentStatus.Value);
}
// فیلتر بر اساس وضعیت ارسال
if (request.DeliveryStatus.HasValue)
{
query = query.Where(o => o.DeliveryStatus == request.DeliveryStatus.Value);
}
// جستجو بر اساس موبایل کاربر
if (!string.IsNullOrWhiteSpace(request.UserMobile))
{
query = query.Where(o => o.User.Mobile.Contains(request.UserMobile));
}
// جستجو بر اساس کد رهگیری
if (!string.IsNullOrWhiteSpace(request.TrackingCode))
{
query = query.Where(o => o.TrackingCode != null && o.TrackingCode.Contains(request.TrackingCode));
}
// فیلتر از تاریخ
if (request.FromDate.HasValue)
{
query = query.Where(o => o.Created >= request.FromDate.Value);
}
// فیلتر تا تاریخ
if (request.ToDate.HasValue)
{
query = query.Where(o => o.Created <= request.ToDate.Value);
}
// فیلتر حداقل مبلغ
if (request.MinAmount.HasValue)
{
query = query.Where(o => o.TotalAmount >= request.MinAmount.Value);
}
// فیلتر حداکثر مبلغ
if (request.MaxAmount.HasValue)
{
query = query.Where(o => o.TotalAmount <= request.MaxAmount.Value);
}
var totalCount = await query.CountAsync(cancellationToken);
// Apply pagination
var pagination = request.PaginationQuery ?? new PaginationState { PageNumber = 1, PageSize = 20 };
var orders = await query
.OrderByDescending(o => o.Created)
.Skip((pagination.PageNumber - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.Select(o => new AdminOrderDto
{
Id = o.Id,
UserId = o.UserId,
UserFullName = (o.User.FirstName ?? "") + " " + (o.User.LastName ?? ""),
UserMobile = o.User.Mobile,
TotalAmount = o.TotalAmount,
DiscountBalanceUsed = o.DiscountBalanceUsed,
GatewayAmountPaid = o.GatewayAmountPaid,
VatAmount = o.VatAmount,
PaymentStatus = o.PaymentStatus,
PaymentDate = o.PaymentDate,
DeliveryStatus = o.DeliveryStatus,
DeliveryDate = null, // TODO: Add DeliveryDate to DiscountOrder if needed
ShippingAddress = o.UserAddress.Address,
ReceiverName = o.UserAddress.Title,
ReceiverMobile = o.User.Mobile,
TrackingCode = o.TrackingCode,
AdminNote = o.DeliveryDescription,
Created = o.Created,
LastModified = o.LastModified,
ItemsCount = o.OrderDetails.Count
})
.ToListAsync(cancellationToken);
return new GetAllDiscountOrdersResponseDto
{
MetaData = new MetaData
{
TotalCount = totalCount,
PageSize = pagination.PageSize,
CurrentPage = pagination.PageNumber,
TotalPage = (int)Math.Ceiling(totalCount / (double)pagination.PageSize)
},
Models = orders
};
}
}
@@ -0,0 +1,21 @@
using MediatR;
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountProductImages;
public class GetDiscountProductImagesQuery : IRequest<List<DiscountProductImageDto>>
{
public long DiscountProductId { get; set; }
public bool OnlyActive { get; set; } = true;
}
public class DiscountProductImageDto
{
public long Id { get; set; }
public long DiscountProductId { get; set; }
public string ImagePath { get; set; } = string.Empty;
public string ThumbnailPath { get; set; } = string.Empty;
public string? Title { get; set; }
public string? AltText { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; }
}
@@ -0,0 +1,39 @@
using CMSMicroservice.Application.Common.Interfaces;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountProductImages;
public class GetDiscountProductImagesQueryHandler : IRequestHandler<GetDiscountProductImagesQuery, List<DiscountProductImageDto>>
{
private readonly IApplicationDbContext _context;
public GetDiscountProductImagesQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<List<DiscountProductImageDto>> Handle(GetDiscountProductImagesQuery request, CancellationToken cancellationToken)
{
var query = _context.DiscountProductImages
.Where(i => i.DiscountProductId == request.DiscountProductId);
if (request.OnlyActive)
query = query.Where(i => i.IsActive);
return await query
.OrderBy(i => i.SortOrder)
.Select(i => new DiscountProductImageDto
{
Id = i.Id,
DiscountProductId = i.DiscountProductId,
ImagePath = i.ImagePath,
ThumbnailPath = i.ThumbnailPath,
Title = i.Title,
AltText = i.AltText,
SortOrder = i.SortOrder,
IsActive = i.IsActive
})
.ToListAsync(cancellationToken);
}
}
@@ -0,0 +1,166 @@
using CMSMicroservice.Application.Common.Models;
using MediatR;
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountSalesReport;
/// <summary>
/// کوئری گزارش فروش فروشگاه تخفیفی
/// </summary>
public class GetDiscountSalesReportQuery : IRequest<DiscountSalesReportDto>
{
/// <summary>
/// از تاریخ
/// </summary>
public DateTime? FromDate { get; set; }
/// <summary>
/// تا تاریخ
/// </summary>
public DateTime? ToDate { get; set; }
/// <summary>
/// نوع گزارش
/// </summary>
public SalesReportType ReportType { get; set; } = SalesReportType.Summary;
}
public enum SalesReportType
{
/// <summary>
/// خلاصه کلی
/// </summary>
Summary,
/// <summary>
/// روزانه
/// </summary>
Daily,
/// <summary>
/// هفتگی
/// </summary>
Weekly,
/// <summary>
/// ماهانه
/// </summary>
Monthly
}
public class DiscountSalesReportDto
{
// خلاصه کلی
public SalesSummary Summary { get; set; } = new();
// جزئیات زمانی (برای گزارش‌های روزانه، هفتگی، ماهانه)
public List<SalesPeriodDto> Periods { get; set; } = new();
// پرفروش‌ترین محصولات
public List<TopSellingProductDto> TopProducts { get; set; } = new();
}
public class SalesSummary
{
/// <summary>
/// تعداد کل سفارشات
/// </summary>
public int TotalOrders { get; set; }
/// <summary>
/// تعداد سفارشات موفق (پرداخت شده)
/// </summary>
public int CompletedOrders { get; set; }
/// <summary>
/// تعداد سفارشات در انتظار پرداخت
/// </summary>
public int PendingOrders { get; set; }
/// <summary>
/// تعداد سفارشات لغو شده
/// </summary>
public int CancelledOrders { get; set; }
/// <summary>
/// جمع کل فروش (TotalAmount)
/// </summary>
public long TotalSalesAmount { get; set; }
/// <summary>
/// جمع تخفیف استفاده شده (DiscountBalanceUsed)
/// </summary>
public long TotalDiscountUsed { get; set; }
/// <summary>
/// جمع پرداخت از درگاه (GatewayAmountPaid)
/// </summary>
public long TotalGatewayPaid { get; set; }
/// <summary>
/// جمع VAT
/// </summary>
public long TotalVatAmount { get; set; }
/// <summary>
/// میانگین ارزش سفارش
/// </summary>
public long AverageOrderValue { get; set; }
/// <summary>
/// تعداد کاربران یکتا
/// </summary>
public int UniqueCustomers { get; set; }
/// <summary>
/// تعداد کل محصولات فروخته شده
/// </summary>
public int TotalProductsSold { get; set; }
}
public class SalesPeriodDto
{
/// <summary>
/// نام دوره (مثل: 1403/10/11 یا هفته 41)
/// </summary>
public string PeriodLabel { get; set; } = string.Empty;
/// <summary>
/// تاریخ شروع دوره
/// </summary>
public DateTime PeriodStart { get; set; }
/// <summary>
/// تاریخ پایان دوره
/// </summary>
public DateTime PeriodEnd { get; set; }
/// <summary>
/// تعداد سفارشات
/// </summary>
public int OrdersCount { get; set; }
/// <summary>
/// جمع فروش
/// </summary>
public long TotalAmount { get; set; }
/// <summary>
/// جمع تخفیف
/// </summary>
public long DiscountUsed { get; set; }
/// <summary>
/// جمع پرداخت درگاه
/// </summary>
public long GatewayPaid { get; set; }
}
public class TopSellingProductDto
{
public long ProductId { get; set; }
public string ProductTitle { get; set; } = string.Empty;
public string? ImagePath { get; set; }
public int QuantitySold { get; set; }
public long TotalRevenue { get; set; }
public int OrdersCount { get; set; }
}
@@ -0,0 +1,193 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Globalization;
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountSalesReport;
public class GetDiscountSalesReportQueryHandler : IRequestHandler<GetDiscountSalesReportQuery, DiscountSalesReportDto>
{
private readonly IApplicationDbContext _context;
private static readonly PersianCalendar PersianCalendar = new();
public GetDiscountSalesReportQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<DiscountSalesReportDto> Handle(GetDiscountSalesReportQuery request, CancellationToken cancellationToken)
{
var result = new DiscountSalesReportDto();
// Base query for orders
var ordersQuery = _context.DiscountOrders.AsQueryable();
// Apply date filters
if (request.FromDate.HasValue)
{
ordersQuery = ordersQuery.Where(o => o.Created >= request.FromDate.Value);
}
if (request.ToDate.HasValue)
{
ordersQuery = ordersQuery.Where(o => o.Created <= request.ToDate.Value);
}
// Get all matching orders for summary
var orders = await ordersQuery.ToListAsync(cancellationToken);
var completedOrders = orders.Where(o => o.PaymentStatus == PaymentStatus.Success).ToList();
// Calculate summary
result.Summary = new SalesSummary
{
TotalOrders = orders.Count,
CompletedOrders = completedOrders.Count,
PendingOrders = orders.Count(o => o.PaymentStatus == PaymentStatus.Pending),
CancelledOrders = orders.Count(o => o.PaymentStatus == PaymentStatus.Reject),
TotalSalesAmount = completedOrders.Sum(o => o.TotalAmount),
TotalDiscountUsed = completedOrders.Sum(o => o.DiscountBalanceUsed),
TotalGatewayPaid = completedOrders.Sum(o => o.GatewayAmountPaid),
TotalVatAmount = completedOrders.Sum(o => o.VatAmount),
AverageOrderValue = completedOrders.Any() ? (long)completedOrders.Average(o => o.TotalAmount) : 0,
UniqueCustomers = orders.Select(o => o.UserId).Distinct().Count()
};
// Get total products sold
var orderIds = completedOrders.Select(o => o.Id).ToList();
result.Summary.TotalProductsSold = await _context.DiscountOrderDetails
.Where(d => orderIds.Contains(d.DiscountOrderId))
.SumAsync(d => d.Count, cancellationToken);
// Generate period-based reports
if (request.ReportType != SalesReportType.Summary && completedOrders.Any())
{
result.Periods = GeneratePeriodReport(completedOrders, request.ReportType);
}
// Get top selling products
result.TopProducts = await GetTopSellingProducts(orderIds, cancellationToken);
return result;
}
private List<SalesPeriodDto> GeneratePeriodReport(List<Domain.Entities.DiscountShop.DiscountOrder> orders, SalesReportType reportType)
{
var periods = new List<SalesPeriodDto>();
var groupedOrders = reportType switch
{
SalesReportType.Daily => orders.GroupBy(o => o.Created.Date),
SalesReportType.Weekly => orders.GroupBy(o => GetStartOfWeek(o.Created)),
SalesReportType.Monthly => orders.GroupBy(o => new DateTime(o.Created.Year, o.Created.Month, 1)),
_ => orders.GroupBy(o => o.Created.Date)
};
foreach (var group in groupedOrders.OrderBy(g => g.Key))
{
var periodStart = group.Key;
var periodEnd = reportType switch
{
SalesReportType.Daily => periodStart.AddDays(1).AddSeconds(-1),
SalesReportType.Weekly => periodStart.AddDays(7).AddSeconds(-1),
SalesReportType.Monthly => periodStart.AddMonths(1).AddSeconds(-1),
_ => periodStart.AddDays(1).AddSeconds(-1)
};
periods.Add(new SalesPeriodDto
{
PeriodLabel = GetPersianPeriodLabel(periodStart, reportType),
PeriodStart = periodStart,
PeriodEnd = periodEnd,
OrdersCount = group.Count(),
TotalAmount = group.Sum(o => o.TotalAmount),
DiscountUsed = group.Sum(o => o.DiscountBalanceUsed),
GatewayPaid = group.Sum(o => o.GatewayAmountPaid)
});
}
return periods;
}
private async Task<List<TopSellingProductDto>> GetTopSellingProducts(List<long> orderIds, CancellationToken cancellationToken)
{
if (!orderIds.Any())
return new List<TopSellingProductDto>();
var topProducts = await _context.DiscountOrderDetails
.Where(d => orderIds.Contains(d.DiscountOrderId))
.GroupBy(d => d.ProductId)
.Select(g => new
{
ProductId = g.Key,
QuantitySold = g.Sum(d => d.Count),
TotalRevenue = g.Sum(d => d.FinalPrice),
OrdersCount = g.Select(d => d.DiscountOrderId).Distinct().Count()
})
.OrderByDescending(x => x.QuantitySold)
.Take(10)
.ToListAsync(cancellationToken);
var productIds = topProducts.Select(p => p.ProductId).ToList();
var products = await _context.DiscountProducts
.Where(p => productIds.Contains(p.Id))
.Select(p => new { p.Id, p.Title, p.ThumbnailPath })
.ToListAsync(cancellationToken);
return topProducts.Select(tp => new TopSellingProductDto
{
ProductId = tp.ProductId,
ProductTitle = products.FirstOrDefault(p => p.Id == tp.ProductId)?.Title ?? "نامشخص",
ImagePath = products.FirstOrDefault(p => p.Id == tp.ProductId)?.ThumbnailPath,
QuantitySold = tp.QuantitySold,
TotalRevenue = tp.TotalRevenue,
OrdersCount = tp.OrdersCount
}).ToList();
}
private static DateTime GetStartOfWeek(DateTime date)
{
// شروع هفته از شنبه (Saturday = 6 in DayOfWeek)
var diff = ((int)date.DayOfWeek + 1) % 7; // Saturday = 0
return date.AddDays(-diff).Date;
}
private static string GetPersianPeriodLabel(DateTime date, SalesReportType reportType)
{
var persianYear = PersianCalendar.GetYear(date);
var persianMonth = PersianCalendar.GetMonth(date);
var persianDay = PersianCalendar.GetDayOfMonth(date);
return reportType switch
{
SalesReportType.Daily => $"{persianYear}/{persianMonth:D2}/{persianDay:D2}",
SalesReportType.Weekly => $"هفته {GetPersianWeekOfYear(date)} - {persianYear}",
SalesReportType.Monthly => $"{GetPersianMonthName(persianMonth)} {persianYear}",
_ => $"{persianYear}/{persianMonth:D2}/{persianDay:D2}"
};
}
private static int GetPersianWeekOfYear(DateTime date)
{
var firstDayOfYear = PersianCalendar.ToDateTime(PersianCalendar.GetYear(date), 1, 1, 0, 0, 0, 0);
var daysSinceStart = (date - firstDayOfYear).Days;
return (daysSinceStart / 7) + 1;
}
private static string GetPersianMonthName(int month) => month switch
{
1 => "فروردین",
2 => "اردیبهشت",
3 => "خرداد",
4 => "تیر",
5 => "مرداد",
6 => "شهریور",
7 => "مهر",
8 => "آبان",
9 => "آذر",
10 => "دی",
11 => "بهمن",
12 => "اسفند",
_ => "نامشخص"
};
}