PartnerAppService.cs 13.7 KB
using FoodLabeling.Application.Helpers;
using FoodLabeling.Application.Contracts.Dtos.Common;
using FoodLabeling.Application.Contracts.Dtos.Partner;
using FoodLabeling.Application.Contracts.IServices;
using FoodLabeling.Application.Services.DbModels;
using Microsoft.AspNetCore.Mvc;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SqlSugar;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Guids;
using Volo.Abp.Uow;
using Yi.Framework.SqlSugarCore.Abstractions;

namespace FoodLabeling.Application.Services;

/// <summary>
/// 合作伙伴管理(fl_partner)
/// </summary>
public class PartnerAppService : ApplicationService, IPartnerAppService
{
    private const int ExportPdfMaxRows = 5000;

    private readonly ISqlSugarDbContext _dbContext;
    private readonly IGuidGenerator _guidGenerator;

    public PartnerAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator)
    {
        _dbContext = dbContext;
        _guidGenerator = guidGenerator;
    }

    /// <inheritdoc />
    public async Task<PagedResultWithPageDto<PartnerGetListOutputDto>> GetListAsync(PartnerGetListInputVo input)
    {
        RefAsync<int> total = 0;
        var query = await BuildPartnerListQueryAsync(input);

        var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
        var items = entities.Select(MapListItem).ToList();
        return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items);
    }

    /// <inheritdoc />
    public async Task<PartnerGetOutputDto> GetAsync(Guid id)
    {
        if (id == Guid.Empty)
        {
            throw new UserFriendlyException("Partner id is required.");
        }

        var partnerId = id.ToString();
        var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
            .FirstAsync(x => !x.IsDeleted && x.Id == partnerId);
        if (entity is null)
        {
            throw new UserFriendlyException("Partner not found.");
        }

        return MapDetail(entity);
    }

    /// <inheritdoc />
    [UnitOfWork]
    public async Task<PartnerGetOutputDto> CreateAsync(PartnerCreateInputVo input)
    {
        var name = input.PartnerName?.Trim();
        if (string.IsNullOrWhiteSpace(name))
        {
            throw new UserFriendlyException("Partner name is required.");
        }

        var email = input.ContactEmail?.Trim();
        if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email))
        {
            throw new UserFriendlyException("Invalid contact email format.");
        }

        var now = Clock.Now;
        var entity = new FlPartnerDbEntity
        {
            Id = _guidGenerator.Create().ToString(),
            IsDeleted = false,
            PartnerName = name,
            ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email,
            PhoneNumber = TrimToNull(input.PhoneNumber),
            State = input.State,
            CreationTime = now,
            CreatorId = CurrentUser?.Id?.ToString(),
            LastModificationTime = now,
            LastModifierId = CurrentUser?.Id?.ToString()
        };
        ApplyAddressFields(entity, input);

        await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
        return await GetAsync(Guid.Parse(entity.Id));
    }

    /// <inheritdoc />
    [UnitOfWork]
    public async Task<PartnerGetOutputDto> UpdateAsync(Guid id, PartnerUpdateInputVo input)
    {
        if (id == Guid.Empty)
        {
            throw new UserFriendlyException("Partner id is required.");
        }

        var partnerId = id.ToString();
        var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
            .FirstAsync(x => !x.IsDeleted && x.Id == partnerId);
        if (entity is null)
        {
            throw new UserFriendlyException("Partner not found.");
        }

        var name = input.PartnerName?.Trim();
        if (string.IsNullOrWhiteSpace(name))
        {
            throw new UserFriendlyException("Partner name is required.");
        }

        var email = input.ContactEmail?.Trim();
        if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email))
        {
            throw new UserFriendlyException("Invalid contact email format.");
        }

        entity.PartnerName = name;
        entity.ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email;
        entity.PhoneNumber = TrimToNull(input.PhoneNumber);
        entity.State = input.State;
        ApplyAddressFields(entity, input);
        entity.LastModificationTime = Clock.Now;
        entity.LastModifierId = CurrentUser?.Id?.ToString();

        await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
        return await GetAsync(id);
    }

    /// <inheritdoc />
    [UnitOfWork]
    public async Task DeleteAsync(Guid id)
    {
        if (id == Guid.Empty)
        {
            throw new UserFriendlyException("Partner id is required.");
        }

        var partnerId = id.ToString();
        var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
            .FirstAsync(x => !x.IsDeleted && x.Id == partnerId);
        if (entity is null)
        {
            throw new UserFriendlyException("Partner not found.");
        }

        entity.IsDeleted = true;
        entity.LastModificationTime = Clock.Now;
        entity.LastModifierId = CurrentUser?.Id?.ToString();
        await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
    }

    /// <inheritdoc />
    [HttpGet]
    public async Task<IActionResult> ExportPdfAsync([FromQuery] PartnerGetListInputVo input)
    {
        QuestPDF.Settings.License = LicenseType.Community;

        var count = await (await BuildPartnerListQueryAsync(input)).CountAsync();
        var query = await BuildPartnerListQueryAsync(input);
        if (count > ExportPdfMaxRows)
        {
            throw new UserFriendlyException(
                $"Export exceeds the maximum of {ExportPdfMaxRows} rows. Narrow your filters and try again.");
        }

        var rows = await query.Take(ExportPdfMaxRows).ToListAsync();
        var fileName = $"companies_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf";

        var document = Document.Create(container =>
        {
            container.Page(page =>
            {
                page.Margin(28);
                page.DefaultTextStyle(x => x.FontSize(10));
                page.Header().Text("Companies").SemiBold().FontSize(18);
                page.Content().PaddingTop(12).Table(table =>
                {
                    table.ColumnsDefinition(c =>
                    {
                        c.RelativeColumn(2.2f);
                        c.RelativeColumn(2.4f);
                        c.RelativeColumn(1.8f);
                        c.RelativeColumn(1f);
                        c.RelativeColumn(1.6f);
                    });

                    static IContainer CellHeader(IContainer c) =>
                        c.Background(Colors.Grey.Lighten3).Padding(6).DefaultTextStyle(x => x.SemiBold());

                    table.Cell().Element(CellHeader).Text("Company");
                    table.Cell().Element(CellHeader).Text("Contact");
                    table.Cell().Element(CellHeader).Text("Phone");
                    table.Cell().Element(CellHeader).Text("Status");
                    table.Cell().Element(CellHeader).Text("Created");

                    foreach (var e in rows)
                    {
                        var status = e.State ? "active" : "inactive";
                        table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5)
                            .Text(e.PartnerName ?? string.Empty);
                        table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5)
                            .Text(string.IsNullOrWhiteSpace(e.ContactEmail) ? "None" : e.ContactEmail!);
                        table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5)
                            .Text(string.IsNullOrWhiteSpace(e.PhoneNumber) ? "None" : e.PhoneNumber!);
                        table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status);
                        table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5)
                            .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm"));
                    }
                });
            });
        });

        var stream = new MemoryStream();
        document.GeneratePdf(stream);
        stream.Position = 0;
        return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName };
    }

    private async Task<ISugarQueryable<FlPartnerDbEntity>> BuildPartnerListQueryAsync(PartnerGetListInputVo input)
    {
        var scope = await PartnerScopeHelper.ResolvePartnerScopeAsync(CurrentUser, _dbContext);

        var keyword = input.Keyword?.Trim();
        var query = _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
            .Where(x => !x.IsDeleted);

        query = PartnerScopeHelper.ApplyPartnerScope(query, scope);

        query = query
            .WhereIF(input.State != null, x => x.State == input.State)
            .WhereIF(!string.IsNullOrWhiteSpace(keyword),
                x => x.PartnerName.Contains(keyword!) ||
                     (x.ContactEmail != null && x.ContactEmail.Contains(keyword!)) ||
                     (x.PhoneNumber != null && x.PhoneNumber.Contains(keyword!)) ||
                     (x.Street != null && x.Street.Contains(keyword!)) ||
                     (x.City != null && x.City.Contains(keyword!)) ||
                     (x.StateCode != null && x.StateCode.Contains(keyword!)) ||
                     (x.Country != null && x.Country.Contains(keyword!)) ||
                     (x.ZipCode != null && x.ZipCode.Contains(keyword!)));

        if (!string.IsNullOrWhiteSpace(input.Sorting))
        {
            var sorting = input.Sorting.Trim();
            if (sorting.Equals("PartnerName desc", StringComparison.OrdinalIgnoreCase))
            {
                query = query.OrderByDescending(x => x.PartnerName);
            }
            else if (sorting.Equals("PartnerName asc", StringComparison.OrdinalIgnoreCase))
            {
                query = query.OrderBy(x => x.PartnerName);
            }
            else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase))
            {
                query = query.OrderByDescending(x => x.CreationTime);
            }
            else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase))
            {
                query = query.OrderBy(x => x.CreationTime);
            }
            else if (sorting.Equals("State desc", StringComparison.OrdinalIgnoreCase))
            {
                query = query.OrderByDescending(x => x.State);
            }
            else if (sorting.Equals("State asc", StringComparison.OrdinalIgnoreCase))
            {
                query = query.OrderBy(x => x.State);
            }
            else
            {
                query = query.OrderByDescending(x => x.CreationTime);
            }
        }
        else
        {
            query = query.OrderByDescending(x => x.CreationTime);
        }

        return query;
    }

    private static PartnerGetListOutputDto MapListItem(FlPartnerDbEntity x)
    {
        var dto = new PartnerGetListOutputDto
        {
            Id = x.Id,
            PartnerName = x.PartnerName,
            ContactEmail = x.ContactEmail,
            PhoneNumber = x.PhoneNumber,
            State = x.State,
            CreationTime = x.CreationTime
        };
        MapAddressFields(dto, x);
        return dto;
    }

    private static PartnerGetOutputDto MapDetail(FlPartnerDbEntity x)
    {
        var dto = new PartnerGetOutputDto
        {
            Id = x.Id,
            PartnerName = x.PartnerName,
            ContactEmail = x.ContactEmail,
            PhoneNumber = x.PhoneNumber,
            State = x.State,
            CreationTime = x.CreationTime,
            LastModificationTime = x.LastModificationTime
        };
        MapAddressFields(dto, x);
        return dto;
    }

    private static void ApplyAddressFields(FlPartnerDbEntity entity, PartnerAddressFieldsDto input)
    {
        entity.Street = TrimToNull(input.Street);
        entity.City = TrimToNull(input.City);
        entity.StateCode = TrimToNull(input.StateCode);
        entity.Country = TrimToNull(input.Country);
        entity.ZipCode = TrimToNull(input.ZipCode);
    }

    private static void MapAddressFields(PartnerAddressFieldsDto dto, FlPartnerDbEntity entity)
    {
        dto.Street = entity.Street;
        dto.City = entity.City;
        dto.StateCode = entity.StateCode;
        dto.Country = entity.Country;
        dto.ZipCode = entity.ZipCode;
    }

    private static string? TrimToNull(string? value)
    {
        var t = value?.Trim();
        return string.IsNullOrEmpty(t) ? null : t;
    }

    private static bool IsPlausibleEmail(string email)
    {
        if (email.Length > 256)
        {
            return false;
        }

        var at = email.IndexOf('@');
        return at > 0 && at < email.Length - 1 && email.IndexOf('@', at + 1) < 0;
    }

    private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total,
        List<T> items)
    {
        var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
        var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount);
        var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize);
        return new PagedResultWithPageDto<T>
        {
            PageIndex = pageIndex,
            PageSize = pageSize,
            TotalCount = total,
            TotalPages = totalPages,
            Items = items
        };
    }
}