using System.IO; using FoodLabeling.Application.Helpers; using FoodLabeling.Application.Contracts.Dtos.Location; using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Options; using FoodLabeling.Domain.Entities; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; using Yi.Framework.SqlSugarCore.Abstractions; namespace FoodLabeling.Application.Services; /// /// 门店管理服务(美国版) /// public class LocationAppService : ApplicationService, ILocationAppService { private readonly ISqlSugarRepository _locationRepository; private readonly ISqlSugarDbContext _dbContext; private readonly IOptionsSnapshot _batchImportOptions; public LocationAppService( ISqlSugarRepository locationRepository, ISqlSugarDbContext dbContext, IOptionsSnapshot batchImportOptions) { _locationRepository = locationRepository; _dbContext = dbContext; _batchImportOptions = batchImportOptions; } /// public async Task> GetListAsync([FromQuery] LocationGetListInputVo input) { RefAsync total = 0; var query = await BuildFilteredQueryAsync(input); if (!string.IsNullOrWhiteSpace(input.Sorting)) { query = query.OrderBy(input.Sorting); } else { query = query.OrderBy(x => x.CreationTime, OrderByType.Desc); } var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); var items = entities.Select(ToListDto).ToList(); var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount; var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total.Value / (double)pageSize); return new PagedResultWithPageDto { PageIndex = pageIndex, PageSize = pageSize, TotalCount = total.Value, TotalPages = totalPages, Items = items }; } /// public async Task CreateAsync([FromBody] LocationCreateInputVo input) { var locationCode = input.LocationCode?.Trim(); if (string.IsNullOrWhiteSpace(locationCode)) { throw new UserFriendlyException("Location ID不能为空"); } var locationName = input.LocationName?.Trim(); if (string.IsNullOrWhiteSpace(locationName)) { throw new UserFriendlyException("Location Name不能为空"); } var isExist = await _locationRepository.IsAnyAsync(x => x.LocationCode == locationCode); if (isExist) { throw new UserFriendlyException("Location ID已存在"); } var entity = new LocationAggregateRoot(GuidGenerator.Create()) { Partner = input.Partner?.Trim(), GroupName = input.GroupName?.Trim(), LocationCode = locationCode, LocationName = locationName, Street = input.Street?.Trim(), City = input.City?.Trim(), StateCode = input.StateCode?.Trim(), Country = input.Country?.Trim(), ZipCode = input.ZipCode?.Trim(), Phone = input.Phone?.Trim(), Email = input.Email?.Trim(), Latitude = input.Latitude, Longitude = input.Longitude, OperatingHours = input.OperatingHours?.Trim(), State = input.State }; await _locationRepository.InsertAsync(entity); return ToListDto(entity); } /// public async Task UpdateAsync(Guid id, [FromBody] LocationUpdateInputVo input) { var entity = await _locationRepository.GetSingleAsync(x => x.Id == id && x.IsDeleted == false); if (entity is null) { throw new UserFriendlyException("门店不存在"); } var locationName = input.LocationName?.Trim(); if (string.IsNullOrWhiteSpace(locationName)) { throw new UserFriendlyException("Location Name不能为空"); } entity.Partner = input.Partner?.Trim(); entity.GroupName = input.GroupName?.Trim(); entity.LocationName = locationName; entity.Street = input.Street?.Trim(); entity.City = input.City?.Trim(); entity.StateCode = input.StateCode?.Trim(); entity.Country = input.Country?.Trim(); entity.ZipCode = input.ZipCode?.Trim(); entity.Phone = input.Phone?.Trim(); entity.Email = input.Email?.Trim(); entity.Latitude = input.Latitude; entity.Longitude = input.Longitude; entity.OperatingHours = input.OperatingHours?.Trim(); entity.State = input.State; await _locationRepository.UpdateAsync(entity); return ToListDto(entity); } /// public async Task DeleteAsync(Guid id) { var entity = await _locationRepository.GetSingleAsync(x => x.Id == id && x.IsDeleted == false); if (entity is null) { throw new UserFriendlyException("门店不存在"); } entity.IsDeleted = true; await _locationRepository.UpdateAsync(entity); } /// public Task DownloadLocationImportTemplateAsync() { var opt = _batchImportOptions.Value; var dir = opt.TemplateDirectory?.Trim(); if (string.IsNullOrWhiteSpace(dir)) { throw new UserFriendlyException("未配置批量导入模板目录 FoodLabeling:BatchImport:TemplateDirectory"); } var fileName = opt.LocationTemplateFileName?.Trim(); if (string.IsNullOrWhiteSpace(fileName)) { throw new UserFriendlyException("未配置模板文件名 FoodLabeling:BatchImport:LocationTemplateFileName"); } var fullPath = Path.Combine(dir, fileName); if (!File.Exists(fullPath)) { throw new UserFriendlyException($"模板文件不存在:{fullPath}"); } var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read); const string contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; return Task.FromResult(new FileStreamResult(stream, contentType) { FileDownloadName = fileName }); } /// public async Task ExportLocationsExcelAsync([FromQuery] LocationGetListInputVo input) { var exportFilter = new LocationGetListInputVo { Sorting = input.Sorting, Keyword = input.Keyword, Partner = input.Partner, GroupName = input.GroupName, State = input.State }; var query = await BuildFilteredQueryAsync(exportFilter); if (!string.IsNullOrWhiteSpace(exportFilter.Sorting)) { query = query.OrderBy(exportFilter.Sorting); } else { query = query.OrderBy(x => x.CreationTime, OrderByType.Desc); } var entities = await query.ToListAsync(); var rows = entities.Select(ToListDto).ToList(); var ms = LocationBatchExcelHelper.BuildExportWorkbook(rows); const string contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; var downloadName = $"locations-export-{Clock.Now:yyyyMMdd-HHmmss}.xlsx"; return new FileStreamResult(ms, contentType) { FileDownloadName = downloadName }; } /// public async Task ImportLocationsBatchAsync([FromForm] LocationBatchImportInputVo input) { if (input?.File is null || input.File.Length == 0) { throw new UserFriendlyException("请上传 Excel 文件(form 字段名:file)"); } var opt = _batchImportOptions.Value; if (input.File.Length > opt.MaxUploadBytes) { throw new UserFriendlyException($"文件过大,最大允许 {opt.MaxUploadBytes / 1024 / 1024} MB"); } var ext = Path.GetExtension(input.File.FileName)?.ToLowerInvariant(); if (ext != ".xlsx") { throw new UserFriendlyException("仅支持 .xlsx 格式的 Excel 文件"); } await using var uploadStream = input.File.OpenReadStream(); var parseErrors = new List(); var rows = LocationBatchExcelHelper.ParseImportWorkbook( uploadStream, opt.MaxImportRows <= 0 ? 5000 : opt.MaxImportRows, out var headerErrors); parseErrors.AddRange(headerErrors); var result = new LocationBatchImportResultDto(); if (rows.Count == 0 && parseErrors.Count > 0) { result.Errors = parseErrors; result.FailCount = parseErrors.Count; return result; } foreach (var (rowNum, vo) in rows) { try { await CreateAsync(vo); result.SuccessCount++; } catch (UserFriendlyException ex) { result.FailCount++; result.Errors.Add(new LocationBatchImportErrorDto { RowNumber = rowNum, LocationCode = vo.LocationCode, Message = ex.Message }); } } result.Errors.InsertRange(0, parseErrors); return result; } /// public async Task UpdateLocationsBulkAsync([FromBody] LocationBulkUpdateInputVo input) { if (input?.Items is null || input.Items.Count == 0) { throw new UserFriendlyException("请至少提交一条编辑数据(items 不能为空)"); } var opt = _batchImportOptions.Value; var maxItems = opt.MaxBulkUpdateItems <= 0 ? 500 : opt.MaxBulkUpdateItems; if (input.Items.Count > maxItems) { throw new UserFriendlyException($"单次批量编辑最多允许 {maxItems} 条,请分批提交"); } var effectiveCount = input.Items.Count(static x => x is not null && x.Id != Guid.Empty); if (effectiveCount == 0) { throw new UserFriendlyException("没有有效的门店 Id(请为待保存行填写 id,空行请使用 id 为空 GUID 或从列表中移除)"); } var result = new LocationBulkUpdateResultDto(); for (var i = 0; i < input.Items.Count; i++) { var item = input.Items[i]; if (item is null || item.Id == Guid.Empty) { continue; } try { await UpdateAsync(item.Id, item); result.SuccessCount++; } catch (UserFriendlyException ex) { result.FailCount++; result.Errors.Add(new LocationBulkUpdateErrorDto { RowNumber = i + 1, Id = item.Id, Message = ex.Message }); } } return result; } private async Task> BuildFilteredQueryAsync(LocationGetListInputVo input) { var scope = await LocationRegionScopeHelper.ResolveLocationListScopeAsync(CurrentUser, _dbContext); var keyword = input.Keyword?.Trim(); var partner = input.Partner?.Trim(); var groupName = input.GroupName?.Trim(); var query = _locationRepository._DbQueryable .Where(x => x.IsDeleted == false); query = LocationRegionScopeHelper.ApplyLocationListScope(query, scope); return query .WhereIF(!string.IsNullOrEmpty(partner), x => x.Partner == partner) .WhereIF(!string.IsNullOrEmpty(groupName), x => x.GroupName == groupName) .WhereIF(input.State is not null, x => x.State == input.State) .WhereIF(!string.IsNullOrEmpty(keyword), x => x.LocationCode.Contains(keyword!) || x.LocationName.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!)) || (x.Phone != null && x.Phone.Contains(keyword!)) || (x.Email != null && x.Email.Contains(keyword!)) || (x.OperatingHours != null && x.OperatingHours.Contains(keyword!))); } private static LocationGetListOutputDto ToListDto(LocationAggregateRoot x) => new() { Id = x.Id, Partner = x.Partner, GroupName = x.GroupName, LocationCode = x.LocationCode, LocationName = x.LocationName, Street = x.Street, City = x.City, StateCode = x.StateCode, Country = x.Country, ZipCode = x.ZipCode, Phone = x.Phone, Email = x.Email, Latitude = x.Latitude, Longitude = x.Longitude, OperatingHours = x.OperatingHours, State = x.State }; }