using System.Linq.Expressions; using System.Text; using System.Text.Json; using Fake.Application; using Microsoft.AspNetCore.Mvc; using MiniExcelLibs; using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Shared; using Oms.Application.Shared; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; namespace Oms.Application.Services.Orders; public partial class OrderService { private async Task> MapOrderOutputsAsync(IReadOnlyCollection orders) { if (orders.Count == 0) { return []; } var orderIds = orders.Select(x => x.Id).ToArray(); var warehouseIds = orders.Where(x => x.WarehouseId.HasValue).Select(x => x.WarehouseId!.Value).Distinct().ToArray(); var senders = await OrderRepository.GetSendersByOrderIdsAsync(orderIds); var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync(orderIds); var declarations = await OrderRepository.GetDeclarationsByOrderIdsAsync(orderIds); var weights = await OrderRepository.GetWeightsByOrderIdsAsync(orderIds); var warehouses = warehouseIds.Length == 0 ? [] : await WarehouseAddressRepository.GetListAsync(x => warehouseIds.Contains(x.Id)); var senderMap = senders.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.First()); var recipientMap = recipients.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.First()); var declarationMap = declarations.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.ToList()); var weightMap = weights.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.ToList()); var warehouseMap = warehouses.ToDictionary(x => x.Id); return orders.Select(order => { senderMap.TryGetValue(order.Id, out var sender); recipientMap.TryGetValue(order.Id, out var recipient); declarationMap.TryGetValue(order.Id, out var items); weightMap.TryGetValue(order.Id, out var orderWeights); return new OrderOutput { Id = order.Id, ClientNo = order.ClientNo, TrackNo = order.TrackNo, ServiceNo = order.ServiceNo, ProductCode = order.ProductCode, ChannelCode = order.ChannelCode, CargoAttribute = order.CargoAttribute, WarehouseId = order.WarehouseId, Warehouse = order.WarehouseId.HasValue && warehouseMap.TryGetValue(order.WarehouseId.Value, out var warehouse) ? warehouse.WarehouseName : null, CustomerId = order.CustomerId, IsCod = order.IsCod > 0, CodCurrency = order.CodCurrency, CodSum = order.CodSum, OrderStatus = OmsDocHelper.GetOrderStatusName(order.OrderStatus), Status = order.OrderStatus, ReceiveTime = order.ReceiveTime, ShipTime = order.ShipTime, PayTime = order.PayTime, SignTime = order.SignTime, IsIntercept = order.IsIntercept > 0, IsRemote = order.IsRemote > 0, CreateTime = order.CreateTime, CreateBy = order.CreateUserId <= 0 ? null : order.CreateUserId.ToString(), UpdateTime = OmsDocHelper.NullIfDefault(order.UpdateTime), UpdateBy = order.UpdateUserId <= 0 ? null : order.UpdateUserId.ToString(), Sender = MapSender(sender), Receiver = MapReceiver(recipient), Items = (items ?? []).Select(MapDeclaration).ToList(), WeightList = (orderWeights ?? []).Select(MapWeight).ToList() }; }).ToList(); } private async Task> BuildOrderExportRowsAsync(IReadOnlyCollection orders) { var mapped = await MapOrderOutputsAsync(orders); return mapped.Select(x => (object)new { x.Id, x.ClientNo, x.TrackNo, x.ServiceNo, x.ProductCode, x.ChannelCode, x.CargoAttribute, x.WarehouseId, x.Warehouse, x.CustomerId, x.Status, x.OrderStatus, x.ReceiveTime, x.ShipTime, x.PayTime, x.SignTime, x.IsCod, x.CodCurrency, x.CodSum, x.IsIntercept, x.IsRemote, x.CreateTime, x.UpdateTime }).ToList(); } private async Task> GetTemplateOutputsAsync(OrderTemplateListInput input) { var page = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); var keyword = input.Keyword?.Trim(); var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, null); var hasCustomerFilter = customerId.HasValue; var customerIdValue = customerId.GetValueOrDefault(); var predicate = (Expression>)(x => (string.IsNullOrWhiteSpace(input.Type) || x.Type == input.Type) && (string.IsNullOrWhiteSpace(keyword) || x.Name.Contains(keyword) || (x.TemplateKey ?? string.Empty).Contains(keyword)) && (!hasCustomerFilter || x.Type == "standard" || x.CustomerId == customerIdValue)); var templates = await OrderTemplateRepository.GetPagedListAsync( predicate, page, pageSize, new Dictionary { [nameof(OrderTemplate.UpdateTime)] = false, [nameof(OrderTemplate.Id)] = false }); var total = await OrderTemplateRepository.CountAsync(predicate); return new OmsPageResult { Items = templates.Select(x => new OrderTemplateOutput { Id = x.Id, Name = x.Name, Type = x.Type, TemplateKey = x.TemplateKey, CustomerId = x.CustomerId, CreateTime = x.CreateTime, UpdateTime = OmsDocHelper.NullIfDefault(x.UpdateTime) }).ToList(), Total = total }; } private async Task BuildTemplateDownloadAsync(int templateId) { var template = await OrderTemplateRepository.FirstOrDefaultAsync(x => x.Id == templateId); if (template == null) { throw new BusinessException("模板不存在"); } var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, null); if (customerId.HasValue) { EnsureTemplateAccessible(template, customerId.Value); } var rows = new[] { new Dictionary { ["ClientNo"] = "", ["TrackNo"] = "", ["ServiceNo"] = "", ["ProductCode"] = "", ["ChannelCode"] = "", ["CargoAttribute"] = CargoAttributeCodes.General, ["WarehouseId"] = "", ["CustomerId"] = customerId ?? template.CustomerId, ["Length"] = "", ["Width"] = "", ["Height"] = "", ["ActualWeight"] = "", ["VolumetricWeight"] = "", ["ChargeableWeight"] = "", ["Cubic"] = "", ["IsCod"] = "", ["CodCurrency"] = "", ["CodSum"] = "", ["DeclaredCurrency"] = "", ["DeclaredSum"] = "", ["Quantity"] = "" } }; await using var stream = new MemoryStream(); await MiniExcel.SaveAsAsync(stream, rows); stream.Position = 0; return new FileContentResult(stream.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = $"{template.Name}-{DateTime.Now:yyyyMMddHHmmss}.xlsx" }; } private async Task ImportOrdersAsync(OrderUploadInput input) { if (input.File == null || input.File.Length == 0) { throw new BusinessException("上传文件不能为空"); } var template = await OrderTemplateRepository.FirstOrDefaultAsync(x => x.Id == input.TemplateId); if (template == null) { throw new BusinessException("模板不存在"); } var effectiveCustomer = await ResolveEffectiveCustomerAsync(); EnsureTemplateAccessible(template, effectiveCustomer.Id); var task = await OrderTaskRepository.InsertAsync(new OrderTask { TaskName = $"批量下单_{DateTime.Now:yyyyMMddHHmmss}", Quantity = 0, SuccessCount = 0, FailCount = 0, Status = "processing", TemplateId = template.Id, CustomerId = effectiveCustomer.Id }); var quantity = 0; var successCount = 0; var failCount = 0; var rowNumber = 1; var failList = new List(); var rows = await MiniExcel.QueryAsync(input.File.OpenReadStream(), useHeaderRow: true); foreach (var row in rows) { rowNumber++; quantity++; if (row is not IDictionary rowDict) { failCount++; failList.Add(BuildTaskFail(task.Id, rowNumber, "无法解析Excel行数据", row)); continue; } try { await CreateImportOrderAsync(rowDict, effectiveCustomer.Id); successCount++; } catch (Exception ex) { failCount++; failList.Add(BuildTaskFail(task.Id, rowNumber, ex.Message, rowDict)); } } if (failList.Count > 0) { await OrderTaskRepository.InsertFailListAsync(failList); } task.Quantity = quantity; task.SuccessCount = successCount; task.FailCount = failCount; task.Status = "completed"; await OrderTaskRepository.UpdateAsync(task); return new OrderUploadOutput { TaskId = task.Id }; } private async Task CreateImportOrderAsync(IDictionary rowDict, int effectiveCustomerId) { var rowCustomerId = GetIntCell(rowDict, "CustomerId"); if (rowCustomerId is > 0 && rowCustomerId.Value != effectiveCustomerId) { throw new BusinessException("客户ID非法"); } var serviceNo = GetStringCell(rowDict, "ServiceNo"); var resolvedServiceNo = await ResolveServiceNoAsync(serviceNo); var order = await OrderRepository.InsertAsync(new Order { ServiceNo = resolvedServiceNo, ClientNo = GetStringCell(rowDict, "ClientNo"), TrackNo = string.IsNullOrWhiteSpace(GetStringCell(rowDict, "TrackNo")) ? null : GetStringCell(rowDict, "TrackNo"), ProductCode = GetStringCell(rowDict, "ProductCode"), ChannelCode = GetStringCell(rowDict, "ChannelCode"), CargoAttribute = GetStringCell(rowDict, "CargoAttribute") ?? CargoAttributeCodes.General, WarehouseId = GetIntCell(rowDict, "WarehouseId"), CustomerId = effectiveCustomerId, CodCurrency = GetStringCell(rowDict, "CodCurrency"), CodSum = GetDecimalCell(rowDict, "CodSum"), DeclaredCurrency = GetStringCell(rowDict, "DeclaredCurrency"), DeclaredSum = GetDecimalCell(rowDict, "DeclaredSum"), Quantity = GetIntCell(rowDict, "Quantity"), IsCod = GetBoolCell(rowDict, "IsCod") ? (byte)1 : (byte)0, IsIntercept = 0, IsRemote = 0, OrderStatus = OrderStatusCodes.PendingPayment }); var weight = BuildForecastWeight(order.Id, rowDict); if (weight != null) { await OrderRepository.InsertWeightsAsync([weight]); } } private async Task> GetTaskOutputsAsync(OrderTaskListInput input) { var page = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); var keyword = input.Keyword?.Trim(); var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, null); var hasCustomerFilter = customerId.HasValue; var customerIdValue = customerId.GetValueOrDefault(); var predicate = (Expression>)(x => (string.IsNullOrWhiteSpace(input.Status) || x.Status == input.Status) && (string.IsNullOrWhiteSpace(keyword) || x.TaskName.Contains(keyword)) && (!hasCustomerFilter || x.CustomerId == customerIdValue)); var tasks = await OrderTaskRepository.GetPagedListAsync( predicate, page, pageSize, new Dictionary { [nameof(OrderTask.UpdateTime)] = false, [nameof(OrderTask.Id)] = false }); var total = await OrderTaskRepository.CountAsync(predicate); return new OmsPageResult { Items = tasks.Select(x => new OrderTaskOutput { Id = x.Id, TaskName = x.TaskName, Quantity = x.Quantity, Status = x.Status, TemplateId = x.TemplateId, CreateTime = x.CreateTime, UpdateTime = OmsDocHelper.NullIfDefault(x.UpdateTime) }).ToList(), Total = total }; } private async Task GetTaskDetailCoreAsync(int taskId) { var task = await OrderTaskRepository.FirstOrDefaultAsync(x => x.Id == taskId); if (task == null) { throw new BusinessException("任务不存在"); } var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, null); if (customerId.HasValue && task.CustomerId != customerId.Value) { throw new BusinessException("任务不存在"); } var failList = await OrderTaskRepository.GetFailListAsync(task.Id); return new OrderTaskDetailOutput { Id = task.Id, TaskName = task.TaskName, Quantity = task.Quantity, SuccessCount = task.SuccessCount, FailCount = task.FailCount, Status = task.Status, TemplateId = task.TemplateId, CreateTime = task.CreateTime, UpdateTime = OmsDocHelper.NullIfDefault(task.UpdateTime), FailList = failList.Select(x => new OrderTaskFailOutput { Id = x.Id, TaskId = x.TaskId, RowNumber = x.RowNumber, Reason = x.Reason, RawDataJson = OmsDocHelper.DeserializeJsonObject(x.RawDataJson) }).ToList() }; } private async Task CreateOrderPrintFileAsync(OrderOutput order) { var directory = Path.Combine(GetWebRootDirectory(), "uploads", "prints", "orders"); Directory.CreateDirectory(directory); var fileName = $"{order.ServiceNo}-{DateTime.Now:yyyyMMddHHmmss}.html"; var html = $""" {order.ServiceNo}

Order Label

ServiceNo: {order.ServiceNo}

ClientNo: {order.ClientNo}

TrackNo: {order.TrackNo}

Warehouse: {order.Warehouse}

"""; await File.WriteAllTextAsync(Path.Combine(directory, fileName), html, Encoding.UTF8); return $"/uploads/prints/orders/{fileName}"; } private string GetOrderExportDirectory() => Path.Combine(GetWebRootDirectory(), "uploads", "exports", "orders"); private string GetWebRootDirectory() { var webRoot = string.IsNullOrWhiteSpace(WebHostEnvironment.WebRootPath) ? Path.Combine(WebHostEnvironment.ContentRootPath, "wwwroot") : WebHostEnvironment.WebRootPath; Directory.CreateDirectory(webRoot); return webRoot; } private async Task CleanupExpiredFilesAsync(string directory) { if (!Directory.Exists(directory)) { return; } await Task.Run(() => { foreach (var file in Directory.GetFiles(directory)) { if (File.GetCreationTime(file) <= DateTime.Now.AddHours(-24)) { File.Delete(file); } } }); } private static OrderSenderOutput? MapSender(OrderSender? sender) { return sender == null ? null : new OrderSenderOutput { SenderName = sender.SenderName, Company = sender.Company, Phone = sender.Phone, TelePhone = sender.TelePhone, Email = sender.Email, CountryCode = sender.Country, Province = sender.Province, City = sender.City, ZipCode = sender.ZipCode, StreetAddress1 = sender.StreetAddress1, StreetAddress2 = sender.StreetAddress2, StreetAddress3 = sender.StreetAddress3, HouseNumber = sender.HouseNumber }; } private static OrderReceiverOutput? MapReceiver(OrderRecipient? recipient) { return recipient == null ? null : new OrderReceiverOutput { RecipientsName = recipient.RecipientsName, CompanyName = recipient.CompanyName, Phone = recipient.Phone, TelePhone = recipient.TelePhone, Email = recipient.Email, CountryCode = recipient.Country, Province = recipient.Province, City = recipient.City, StreetNumber = recipient.StreetNumber, StreetAddress1 = recipient.StreetAddress1, StreetAddress2 = recipient.StreetAddress2, StreetAddress3 = recipient.StreetAddress3, ZipCode = recipient.ZipCode, TaxType = recipient.TaxType, TaxNo = recipient.TaxNo, CertificateType = recipient.CertificateType, CertificateNo = recipient.CertificateNo, Eori = recipient.Eori, Vat = recipient.Vat }; } private static OrderItemOutput MapDeclaration(OrderDeclaration x) { return new OrderItemOutput { CnName = x.CnName, EnName = x.EnName, Sku = x.Sku, Unit = x.Unit, UnitPrice = x.UnitPrice, UnitWeight = x.UnitWeight, Quantity = x.Quantity, Amount = x.Amount, Weight = x.Weight, HsCode = x.ExHsCode ?? x.ImHsCode ?? x.HsCode, SkuImage = x.SkuImage, Category = x.Category, Brand = x.Brand, BrandType = x.BrandType, IsMagnetic = x.IsMagnetic > 0, Distribution = x.Distribution, Material = x.Material, EnMaterial = x.EnMaterial, Purpose = x.Purpose, Attributes = x.Attributes, OriginCountry = x.OriginCountry, SalesLink = x.SalesLink, Model = x.Model, Specifications = x.Specifications, Comment = x.Comment }; } private static OrderWeightOutput MapWeight(OrderWeight x) { return new OrderWeightOutput { Type = x.Type, Length = x.Length, Width = x.Width, Height = x.Height, ActualWeight = x.ActualWeight, VolumetricWeight = x.VolumetricWeight, ChargeableWeight = x.ChargeableWeight, Cubic = x.Cubic }; } private static OrderTaskFail BuildTaskFail(int taskId, int rowNumber, string reason, object rawData) { return new OrderTaskFail { TaskId = taskId, RowNumber = rowNumber, Reason = reason, RawDataJson = JsonSerializer.Serialize(rawData), CreateUserId = 0, CreateTime = DateTime.Now }; } private static string? GetStringCell(IDictionary row, string key) => row.TryGetValue(key, out var value) ? value?.ToString() : null; private static int? GetintCell(IDictionary row, string key) => int.TryParse(GetStringCell(row, key), out var value) ? value : null; private static int? GetIntCell(IDictionary row, string key) => int.TryParse(GetStringCell(row, key), out var value) ? value : null; private static decimal? GetDecimalCell(IDictionary row, string key) => decimal.TryParse(GetStringCell(row, key), out var value) ? value : null; private static bool GetBoolCell(IDictionary row, string key) => GetStringCell(row, key) is "1" or "true" or "True" or "TRUE" or "是"; private static void EnsureTemplateAccessible(OrderTemplate template, int effectiveCustomerId) { if (string.Equals(template.Type, "standard", StringComparison.OrdinalIgnoreCase)) { return; } if (template.CustomerId != effectiveCustomerId) { throw new BusinessException("模板不存在"); } } private OrderWeight? BuildForecastWeight(int orderId, IDictionary row) { var length = GetDecimalCell(row, "Length"); var width = GetDecimalCell(row, "Width"); var height = GetDecimalCell(row, "Height"); var actualWeight = GetDecimalCell(row, "ActualWeight"); var volumetricWeight = GetDecimalCell(row, "VolumetricWeight"); var chargeableWeight = GetDecimalCell(row, "ChargeableWeight"); var cubic = GetDecimalCell(row, "Cubic"); if (!length.HasValue && !width.HasValue && !height.HasValue && !actualWeight.HasValue && !volumetricWeight.HasValue && !chargeableWeight.HasValue && !cubic.HasValue) { return null; } return new OrderWeight { OrderId = orderId, Type = OrderWeightTypeCodes.Forecast, Length = length, Width = width, Height = height, ActualWeight = actualWeight, VolumetricWeight = volumetricWeight, ChargeableWeight = chargeableWeight, Cubic = cubic }; } }