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
};
}