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; /// /// 合作伙伴管理(fl_partner) /// 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; } /// public async Task> GetListAsync(PartnerGetListInputVo input) { RefAsync 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); } /// public async Task GetAsync(Guid id) { if (id == Guid.Empty) { throw new UserFriendlyException("Partner id is required."); } var partnerId = id.ToString(); var entity = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); if (entity is null) { throw new UserFriendlyException("Partner not found."); } return MapDetail(entity); } /// [UnitOfWork] public async Task 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)); } /// [UnitOfWork] public async Task 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() .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); } /// [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() .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(); } /// [HttpGet] public async Task 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> BuildPartnerListQueryAsync(PartnerGetListInputVo input) { var scope = await PartnerScopeHelper.ResolvePartnerScopeAsync(CurrentUser, _dbContext); var keyword = input.Keyword?.Trim(); var query = _dbContext.SqlSugarClient.Queryable() .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 BuildPagedResult(int skipCount, int maxResultCount, int total, List 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 { PageIndex = pageIndex, PageSize = pageSize, TotalCount = total, TotalPages = totalPages, Items = items }; } }