feat(product): add product list/detail/save/soldout/batch api support
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 47s
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 47s
This commit is contained in:
@@ -64,6 +64,56 @@ public sealed class CreateProductCommand : IRequest<ProductDto>
|
||||
/// </summary>
|
||||
public ProductStatus Status { get; set; } = ProductStatus.Draft;
|
||||
|
||||
/// <summary>
|
||||
/// 商品类型。
|
||||
/// </summary>
|
||||
public ProductKind Kind { get; set; } = ProductKind.Single;
|
||||
|
||||
/// <summary>
|
||||
/// 月销量。
|
||||
/// </summary>
|
||||
public int SalesMonthly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标签 JSON。
|
||||
/// </summary>
|
||||
public string? TagsJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清模式。
|
||||
/// </summary>
|
||||
public ProductSoldoutMode? SoldoutMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清恢复时间。
|
||||
/// </summary>
|
||||
public DateTime? RecoverAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清后剩余可售库存。
|
||||
/// </summary>
|
||||
public int? RemainStock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清原因。
|
||||
/// </summary>
|
||||
public string? SoldoutReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同步通知平台。
|
||||
/// </summary>
|
||||
public bool SyncToPlatform { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否通知店长。
|
||||
/// </summary>
|
||||
public bool NotifyManager { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 定时上架时间。
|
||||
/// </summary>
|
||||
public DateTime? TimedOnShelfAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 主图。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
using TakeoutSaaS.Domain.Products.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 商品沽清命令。
|
||||
/// </summary>
|
||||
public sealed class SoldoutProductCommand : IRequest<ProductDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 商品 ID。
|
||||
/// </summary>
|
||||
public long ProductId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清模式。
|
||||
/// </summary>
|
||||
public ProductSoldoutMode Mode { get; init; } = ProductSoldoutMode.Today;
|
||||
|
||||
/// <summary>
|
||||
/// 剩余可售库存。
|
||||
/// </summary>
|
||||
public int RemainStock { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清原因。
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 恢复时间(定时沽清)。
|
||||
/// </summary>
|
||||
public DateTime? RecoverAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同步平台。
|
||||
/// </summary>
|
||||
public bool SyncToPlatform { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否通知店长。
|
||||
/// </summary>
|
||||
public bool NotifyManager { get; init; }
|
||||
}
|
||||
@@ -69,6 +69,56 @@ public sealed record UpdateProductCommand : IRequest<ProductDto?>
|
||||
/// </summary>
|
||||
public ProductStatus Status { get; init; } = ProductStatus.Draft;
|
||||
|
||||
/// <summary>
|
||||
/// 商品类型。
|
||||
/// </summary>
|
||||
public ProductKind Kind { get; init; } = ProductKind.Single;
|
||||
|
||||
/// <summary>
|
||||
/// 月销量。
|
||||
/// </summary>
|
||||
public int SalesMonthly { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 标签 JSON。
|
||||
/// </summary>
|
||||
public string? TagsJson { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清模式。
|
||||
/// </summary>
|
||||
public ProductSoldoutMode? SoldoutMode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清恢复时间。
|
||||
/// </summary>
|
||||
public DateTime? RecoverAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清后剩余可售库存。
|
||||
/// </summary>
|
||||
public int? RemainStock { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清原因。
|
||||
/// </summary>
|
||||
public string? SoldoutReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同步通知平台。
|
||||
/// </summary>
|
||||
public bool SyncToPlatform { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否通知店长。
|
||||
/// </summary>
|
||||
public bool NotifyManager { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 定时上架时间。
|
||||
/// </summary>
|
||||
public DateTime? TimedOnShelfAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 主图。
|
||||
/// </summary>
|
||||
|
||||
@@ -78,6 +78,56 @@ public sealed class ProductDto
|
||||
/// </summary>
|
||||
public ProductStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品类型。
|
||||
/// </summary>
|
||||
public ProductKind Kind { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 月销量。
|
||||
/// </summary>
|
||||
public int SalesMonthly { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 标签 JSON。
|
||||
/// </summary>
|
||||
public string? TagsJson { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清模式。
|
||||
/// </summary>
|
||||
public ProductSoldoutMode? SoldoutMode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清恢复时间。
|
||||
/// </summary>
|
||||
public DateTime? RecoverAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 剩余可售库存。
|
||||
/// </summary>
|
||||
public int? RemainStock { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 沽清原因。
|
||||
/// </summary>
|
||||
public string? SoldoutReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同步通知平台。
|
||||
/// </summary>
|
||||
public bool SyncToPlatform { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否通知店长。
|
||||
/// </summary>
|
||||
public bool NotifyManager { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 定时上架时间。
|
||||
/// </summary>
|
||||
public DateTime? TimedOnShelfAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 主图。
|
||||
/// </summary>
|
||||
|
||||
@@ -33,6 +33,16 @@ public sealed class CreateProductCommandHandler(IProductRepository productReposi
|
||||
StockQuantity = request.StockQuantity,
|
||||
MaxQuantityPerOrder = request.MaxQuantityPerOrder,
|
||||
Status = request.Status,
|
||||
Kind = request.Kind,
|
||||
SalesMonthly = request.SalesMonthly,
|
||||
TagsJson = request.TagsJson?.Trim(),
|
||||
SoldoutMode = request.SoldoutMode,
|
||||
RecoverAt = request.RecoverAt,
|
||||
RemainStock = request.RemainStock,
|
||||
SoldoutReason = request.SoldoutReason?.Trim(),
|
||||
SyncToPlatform = request.SyncToPlatform,
|
||||
NotifyManager = request.NotifyManager,
|
||||
TimedOnShelfAt = request.TimedOnShelfAt,
|
||||
CoverImage = request.CoverImage?.Trim(),
|
||||
GalleryImages = request.GalleryImages?.Trim(),
|
||||
Description = request.Description?.Trim(),
|
||||
@@ -66,6 +76,16 @@ public sealed class CreateProductCommandHandler(IProductRepository productReposi
|
||||
StockQuantity = product.StockQuantity,
|
||||
MaxQuantityPerOrder = product.MaxQuantityPerOrder,
|
||||
Status = product.Status,
|
||||
Kind = product.Kind,
|
||||
SalesMonthly = product.SalesMonthly,
|
||||
TagsJson = product.TagsJson,
|
||||
SoldoutMode = product.SoldoutMode,
|
||||
RecoverAt = product.RecoverAt,
|
||||
RemainStock = product.RemainStock,
|
||||
SoldoutReason = product.SoldoutReason,
|
||||
SyncToPlatform = product.SyncToPlatform,
|
||||
NotifyManager = product.NotifyManager,
|
||||
TimedOnShelfAt = product.TimedOnShelfAt,
|
||||
CoverImage = product.CoverImage,
|
||||
GalleryImages = product.GalleryImages,
|
||||
Description = product.Description,
|
||||
|
||||
@@ -39,17 +39,21 @@ public sealed class SearchProductPickerQueryHandler(
|
||||
Name = product.Name,
|
||||
Price = product.Price,
|
||||
SpuCode = product.SpuCode,
|
||||
Status = ToPickerStatus(product.Status)
|
||||
Status = ToPickerStatus(product.Status, product.SoldoutMode.HasValue)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string ToPickerStatus(ProductStatus status)
|
||||
private static string ToPickerStatus(ProductStatus status, bool isSoldout)
|
||||
{
|
||||
if (isSoldout)
|
||||
{
|
||||
return "sold_out";
|
||||
}
|
||||
|
||||
return status switch
|
||||
{
|
||||
ProductStatus.OnSale => "on_sale",
|
||||
ProductStatus.Archived => "sold_out",
|
||||
_ => "off_shelf"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,16 +22,49 @@ public sealed class SearchProductsQueryHandler(
|
||||
public async Task<PagedResult<ProductDto>> Handle(SearchProductsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var products = await _productRepository.SearchAsync(tenantId, request.StoreId, request.CategoryId, request.Status, cancellationToken);
|
||||
|
||||
var sorted = ApplySorting(products, request.SortBy, request.SortDescending);
|
||||
// 沽清查询需要包含 OffShelf 状态后再做二次过滤。
|
||||
var repositoryStatus = request.IsSoldOut == true ? null : request.Status;
|
||||
var products = await _productRepository.SearchAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.CategoryId,
|
||||
repositoryStatus,
|
||||
cancellationToken);
|
||||
|
||||
IEnumerable<Domain.Products.Entities.Product> filtered = products;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Keyword))
|
||||
{
|
||||
var keyword = request.Keyword.Trim().ToLowerInvariant();
|
||||
filtered = filtered.Where(item =>
|
||||
item.Name.ToLower().Contains(keyword) ||
|
||||
item.SpuCode.ToLower().Contains(keyword));
|
||||
}
|
||||
|
||||
if (request.Kind.HasValue)
|
||||
{
|
||||
filtered = filtered.Where(item => item.Kind == request.Kind.Value);
|
||||
}
|
||||
|
||||
if (request.IsSoldOut == true)
|
||||
{
|
||||
filtered = filtered.Where(item => item.SoldoutMode.HasValue);
|
||||
}
|
||||
else if (request.Status == Domain.Products.Enums.ProductStatus.OffShelf)
|
||||
{
|
||||
filtered = filtered.Where(item => !item.SoldoutMode.HasValue);
|
||||
}
|
||||
|
||||
var filteredList = filtered.ToList();
|
||||
var sorted = ApplySorting(filteredList, request.SortBy, request.SortDescending);
|
||||
var paged = sorted
|
||||
.Skip((request.Page - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.ToList();
|
||||
|
||||
var items = paged.Select(MapToDto).ToList();
|
||||
return new PagedResult<ProductDto>(items, request.Page, request.PageSize, products.Count);
|
||||
return new PagedResult<ProductDto>(items, request.Page, request.PageSize, filteredList.Count);
|
||||
}
|
||||
|
||||
private static IOrderedEnumerable<Domain.Products.Entities.Product> ApplySorting(
|
||||
@@ -63,6 +96,16 @@ public sealed class SearchProductsQueryHandler(
|
||||
StockQuantity = product.StockQuantity,
|
||||
MaxQuantityPerOrder = product.MaxQuantityPerOrder,
|
||||
Status = product.Status,
|
||||
Kind = product.Kind,
|
||||
SalesMonthly = product.SalesMonthly,
|
||||
TagsJson = product.TagsJson,
|
||||
SoldoutMode = product.SoldoutMode,
|
||||
RecoverAt = product.RecoverAt,
|
||||
RemainStock = product.RemainStock,
|
||||
SoldoutReason = product.SoldoutReason,
|
||||
SyncToPlatform = product.SyncToPlatform,
|
||||
NotifyManager = product.NotifyManager,
|
||||
TimedOnShelfAt = product.TimedOnShelfAt,
|
||||
CoverImage = product.CoverImage,
|
||||
GalleryImages = product.GalleryImages,
|
||||
Description = product.Description,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
using TakeoutSaaS.Application.App.Products;
|
||||
using TakeoutSaaS.Domain.Products.Enums;
|
||||
using TakeoutSaaS.Domain.Products.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 商品沽清命令处理器。
|
||||
/// </summary>
|
||||
public sealed class SoldoutProductCommandHandler(
|
||||
IProductRepository productRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<SoldoutProductCommandHandler> logger)
|
||||
: IRequestHandler<SoldoutProductCommand, ProductDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<ProductDto?> Handle(SoldoutProductCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var product = await productRepository.FindByIdAsync(request.ProductId, tenantId, cancellationToken);
|
||||
if (product is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
product.Status = ProductStatus.OffShelf;
|
||||
product.SoldoutMode = request.Mode;
|
||||
product.RemainStock = Math.Max(0, request.RemainStock);
|
||||
product.StockQuantity = Math.Max(0, request.RemainStock);
|
||||
product.SoldoutReason = string.IsNullOrWhiteSpace(request.Reason) ? null : request.Reason.Trim();
|
||||
product.RecoverAt = request.Mode == ProductSoldoutMode.Timed ? request.RecoverAt : null;
|
||||
product.SyncToPlatform = request.SyncToPlatform;
|
||||
product.NotifyManager = request.NotifyManager;
|
||||
|
||||
await productRepository.UpdateProductAsync(product, cancellationToken);
|
||||
await productRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("商品沽清 {ProductId},模式 {Mode}", product.Id, request.Mode);
|
||||
|
||||
return ProductMapping.ToDto(product);
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,16 @@ public sealed class UpdateProductCommandHandler(
|
||||
existing.StockQuantity = request.StockQuantity;
|
||||
existing.MaxQuantityPerOrder = request.MaxQuantityPerOrder;
|
||||
existing.Status = request.Status;
|
||||
existing.Kind = request.Kind;
|
||||
existing.SalesMonthly = request.SalesMonthly;
|
||||
existing.TagsJson = request.TagsJson?.Trim();
|
||||
existing.SoldoutMode = request.SoldoutMode;
|
||||
existing.RecoverAt = request.RecoverAt;
|
||||
existing.RemainStock = request.RemainStock;
|
||||
existing.SoldoutReason = request.SoldoutReason?.Trim();
|
||||
existing.SyncToPlatform = request.SyncToPlatform;
|
||||
existing.NotifyManager = request.NotifyManager;
|
||||
existing.TimedOnShelfAt = request.TimedOnShelfAt;
|
||||
existing.CoverImage = request.CoverImage?.Trim();
|
||||
existing.GalleryImages = request.GalleryImages?.Trim();
|
||||
existing.Description = request.Description?.Trim();
|
||||
@@ -76,6 +86,16 @@ public sealed class UpdateProductCommandHandler(
|
||||
StockQuantity = product.StockQuantity,
|
||||
MaxQuantityPerOrder = product.MaxQuantityPerOrder,
|
||||
Status = product.Status,
|
||||
Kind = product.Kind,
|
||||
SalesMonthly = product.SalesMonthly,
|
||||
TagsJson = product.TagsJson,
|
||||
SoldoutMode = product.SoldoutMode,
|
||||
RecoverAt = product.RecoverAt,
|
||||
RemainStock = product.RemainStock,
|
||||
SoldoutReason = product.SoldoutReason,
|
||||
SyncToPlatform = product.SyncToPlatform,
|
||||
NotifyManager = product.NotifyManager,
|
||||
TimedOnShelfAt = product.TimedOnShelfAt,
|
||||
CoverImage = product.CoverImage,
|
||||
GalleryImages = product.GalleryImages,
|
||||
Description = product.Description,
|
||||
|
||||
@@ -28,6 +28,16 @@ public static class ProductMapping
|
||||
StockQuantity = product.StockQuantity,
|
||||
MaxQuantityPerOrder = product.MaxQuantityPerOrder,
|
||||
Status = product.Status,
|
||||
Kind = product.Kind,
|
||||
SalesMonthly = product.SalesMonthly,
|
||||
TagsJson = product.TagsJson,
|
||||
SoldoutMode = product.SoldoutMode,
|
||||
RecoverAt = product.RecoverAt,
|
||||
RemainStock = product.RemainStock,
|
||||
SoldoutReason = product.SoldoutReason,
|
||||
SyncToPlatform = product.SyncToPlatform,
|
||||
NotifyManager = product.NotifyManager,
|
||||
TimedOnShelfAt = product.TimedOnShelfAt,
|
||||
CoverImage = product.CoverImage,
|
||||
GalleryImages = product.GalleryImages,
|
||||
Description = product.Description,
|
||||
|
||||
@@ -25,6 +25,21 @@ public sealed class SearchProductsQuery : IRequest<PagedResult<ProductDto>>
|
||||
/// </summary>
|
||||
public ProductStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品类型过滤。
|
||||
/// </summary>
|
||||
public ProductKind? Kind { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字(名称/编码)。
|
||||
/// </summary>
|
||||
public string? Keyword { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否仅沽清商品。
|
||||
/// </summary>
|
||||
public bool? IsSoldOut { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
|
||||
@@ -23,6 +23,10 @@ public sealed class CreateProductCommandValidator : AbstractValidator<CreateProd
|
||||
RuleFor(x => x.OriginalPrice).GreaterThanOrEqualTo(0).When(x => x.OriginalPrice.HasValue);
|
||||
RuleFor(x => x.StockQuantity).GreaterThanOrEqualTo(0).When(x => x.StockQuantity.HasValue);
|
||||
RuleFor(x => x.MaxQuantityPerOrder).GreaterThan(0).When(x => x.MaxQuantityPerOrder.HasValue);
|
||||
RuleFor(x => x.SalesMonthly).GreaterThanOrEqualTo(0);
|
||||
RuleFor(x => x.TagsJson).MaximumLength(2000);
|
||||
RuleFor(x => x.RemainStock).GreaterThanOrEqualTo(0).When(x => x.RemainStock.HasValue);
|
||||
RuleFor(x => x.SoldoutReason).MaximumLength(256);
|
||||
RuleFor(x => x.CoverImage).MaximumLength(256);
|
||||
RuleFor(x => x.GalleryImages).MaximumLength(1024);
|
||||
}
|
||||
|
||||
@@ -16,5 +16,6 @@ public sealed class SearchProductsQueryValidator : AbstractValidator<SearchProdu
|
||||
RuleFor(x => x.Page).GreaterThan(0);
|
||||
RuleFor(x => x.PageSize).InclusiveBetween(1, 200);
|
||||
RuleFor(x => x.SortBy).MaximumLength(64);
|
||||
RuleFor(x => x.Keyword).MaximumLength(64);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
using TakeoutSaaS.Domain.Products.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Products.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 商品沽清命令验证器。
|
||||
/// </summary>
|
||||
public sealed class SoldoutProductCommandValidator : AbstractValidator<SoldoutProductCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public SoldoutProductCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.ProductId).GreaterThan(0);
|
||||
RuleFor(x => x.RemainStock).GreaterThanOrEqualTo(0);
|
||||
RuleFor(x => x.Reason).MaximumLength(256);
|
||||
RuleFor(x => x.RecoverAt)
|
||||
.NotNull()
|
||||
.When(x => x.Mode == ProductSoldoutMode.Timed);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,10 @@ public sealed class UpdateProductCommandValidator : AbstractValidator<UpdateProd
|
||||
RuleFor(x => x.OriginalPrice).GreaterThanOrEqualTo(0).When(x => x.OriginalPrice.HasValue);
|
||||
RuleFor(x => x.StockQuantity).GreaterThanOrEqualTo(0).When(x => x.StockQuantity.HasValue);
|
||||
RuleFor(x => x.MaxQuantityPerOrder).GreaterThan(0).When(x => x.MaxQuantityPerOrder.HasValue);
|
||||
RuleFor(x => x.SalesMonthly).GreaterThanOrEqualTo(0);
|
||||
RuleFor(x => x.TagsJson).MaximumLength(2000);
|
||||
RuleFor(x => x.RemainStock).GreaterThanOrEqualTo(0).When(x => x.RemainStock.HasValue);
|
||||
RuleFor(x => x.SoldoutReason).MaximumLength(256);
|
||||
RuleFor(x => x.CoverImage).MaximumLength(256);
|
||||
RuleFor(x => x.GalleryImages).MaximumLength(1024);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user