diff --git a/db.sql b/db.sql index 72e747b..a800793 100644 --- a/db.sql +++ b/db.sql @@ -124,6 +124,7 @@ CREATE TABLE `t_order` ( `ConsigneeCountryName` VARCHAR(64) NULL COMMENT '国家中文名称', `CargoAttribute` VARCHAR(32) NULL COMMENT '货物属性:general/electronics/sensitive', `ExportCustomsMode` VARCHAR(32) NULL COMMENT '出口报关方式', + `ExportQualificationId` INT NULL COMMENT '出口资质备案 Id(t_customer_export_qualification.Id)', `PredeclareFailReason` VARCHAR(512) NULL COMMENT '预报失败原因', `AllocationPendingReason` VARCHAR(512) NULL COMMENT '待排货原因(排货或渠道买价未匹配)', `AllocationScheme` VARCHAR(32) NULL COMMENT '最近一次排货方案 costLowest/timeFastest/overallBest/designated', @@ -204,6 +205,7 @@ CREATE TABLE `t_order` ( KEY `idx_ord_source_ctime` (`OrderSource`, `CreateTime`), KEY `idx_order_pickup` (`PickupOrderId`), KEY `idx_order_inbound_method` (`InboundMethod`), + KEY `idx_order_export_qualification` (`ExportQualificationId`), KEY `idx_ord_fulfillment_customer` (`FulfillmentType`, `CustomerId`, `OrderStatus`) ) COMMENT='订单表'; diff --git a/sql/migrate_order_export_qualification_id.sql b/sql/migrate_order_export_qualification_id.sql new file mode 100644 index 0000000..7cb5d9b --- /dev/null +++ b/sql/migrate_order_export_qualification_id.sql @@ -0,0 +1,24 @@ +-- 存量库:t_order 增加出口资质备案 Id(9610Summary 选用) +-- 可重复执行。 + +SET @db := DATABASE(); + +SET @exists := ( + SELECT COUNT(1) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db + AND TABLE_NAME = 't_order' + AND COLUMN_NAME = 'ExportQualificationId' +); + +SET @sql := IF( + @exists = 0, + 'ALTER TABLE `t_order` + ADD COLUMN `ExportQualificationId` INT NULL COMMENT ''出口资质备案 Id(t_customer_export_qualification.Id)'' AFTER `ExportCustomsMode`, + ADD KEY `idx_order_export_qualification` (`ExportQualificationId`)', + 'SELECT ''t_order.ExportQualificationId already exists'' AS info' +); + +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/src/Oms.Application/Dtos/Customers/CustomerExportQualificationDtos.cs b/src/Oms.Application/Dtos/Customers/CustomerExportQualificationDtos.cs index d068309..a0268b8 100644 --- a/src/Oms.Application/Dtos/Customers/CustomerExportQualificationDtos.cs +++ b/src/Oms.Application/Dtos/Customers/CustomerExportQualificationDtos.cs @@ -48,18 +48,21 @@ public class CreateCustomerExportQualificationInput [StringLength(200)] public string DomesticConsignor { get; set; } = string.Empty; + [Required] [StringLength(64)] - public string? DomesticConsignorCustomsCode { get; set; } + public string DomesticConsignorCustomsCode { get; set; } = string.Empty; [Required] [StringLength(200)] public string ProducerName { get; set; } = string.Empty; + [Required] [StringLength(64)] - public string? ProducerCustomsCode { get; set; } + public string ProducerCustomsCode { get; set; } = string.Empty; + [Required] [StringLength(100)] - public string? CustomsPort { get; set; } + public string CustomsPort { get; set; } = string.Empty; public bool IsDefault { get; set; } diff --git a/src/Oms.Application/Dtos/Orders/OrderOutput.cs b/src/Oms.Application/Dtos/Orders/OrderOutput.cs index 47b72f4..a82aea9 100644 --- a/src/Oms.Application/Dtos/Orders/OrderOutput.cs +++ b/src/Oms.Application/Dtos/Orders/OrderOutput.cs @@ -205,6 +205,11 @@ public class OrderOutput /// public string? ExportCustomsMode { get; set; } + /// + /// 出口资质备案 Id。 + /// + public int? ExportQualificationId { get; set; } + /// /// 预报失败原因(草稿订单)。 /// diff --git a/src/Oms.Application/Dtos/Orders/OrderParcelCreateInput.cs b/src/Oms.Application/Dtos/Orders/OrderParcelCreateInput.cs index f3048f6..14ab496 100644 --- a/src/Oms.Application/Dtos/Orders/OrderParcelCreateInput.cs +++ b/src/Oms.Application/Dtos/Orders/OrderParcelCreateInput.cs @@ -45,6 +45,11 @@ public class OrderParcelCreateInput /// public string? ExportCustomsMode { get; set; } + /// + /// 出口资质备案 Id(ExportCustomsMode=9610Summary 时必填)。 + /// + public int? ExportQualificationId { get; set; } + /// /// 仓库主键。 /// diff --git a/src/Oms.Application/Services/Customers/CustomerExportQualificationService.cs b/src/Oms.Application/Services/Customers/CustomerExportQualificationService.cs index 4818a7b..16e86e0 100644 --- a/src/Oms.Application/Services/Customers/CustomerExportQualificationService.cs +++ b/src/Oms.Application/Services/Customers/CustomerExportQualificationService.cs @@ -86,10 +86,10 @@ public class CustomerExportQualificationService : ApplicationService CompanyName = input.CompanyName.Trim(), SocialCreditCode = input.SocialCreditCode.Trim(), DomesticConsignor = input.DomesticConsignor.Trim(), - DomesticConsignorCustomsCode = TrimOrNull(input.DomesticConsignorCustomsCode), + DomesticConsignorCustomsCode = input.DomesticConsignorCustomsCode.Trim(), ProducerName = input.ProducerName.Trim(), - ProducerCustomsCode = TrimOrNull(input.ProducerCustomsCode), - CustomsPort = TrimOrNull(input.CustomsPort), + ProducerCustomsCode = input.ProducerCustomsCode.Trim(), + CustomsPort = input.CustomsPort.Trim(), IsDefault = input.IsDefault ? (byte)1 : (byte)0, Remark = TrimOrNull(input.Remark) }; @@ -115,10 +115,10 @@ public class CustomerExportQualificationService : ApplicationService entity.CompanyName = input.CompanyName.Trim(); entity.SocialCreditCode = input.SocialCreditCode.Trim(); entity.DomesticConsignor = input.DomesticConsignor.Trim(); - entity.DomesticConsignorCustomsCode = TrimOrNull(input.DomesticConsignorCustomsCode); + entity.DomesticConsignorCustomsCode = input.DomesticConsignorCustomsCode.Trim(); entity.ProducerName = input.ProducerName.Trim(); - entity.ProducerCustomsCode = TrimOrNull(input.ProducerCustomsCode); - entity.CustomsPort = TrimOrNull(input.CustomsPort); + entity.ProducerCustomsCode = input.ProducerCustomsCode.Trim(); + entity.CustomsPort = input.CustomsPort.Trim(); entity.IsDefault = input.IsDefault ? (byte)1 : (byte)0; entity.Remark = TrimOrNull(input.Remark); @@ -183,10 +183,25 @@ public class CustomerExportQualificationService : ApplicationService throw new BusinessException("境内发货人不能为空"); } + if (string.IsNullOrWhiteSpace(input.DomesticConsignorCustomsCode)) + { + throw new BusinessException("境内发货人海关代码不能为空"); + } + if (string.IsNullOrWhiteSpace(input.ProducerName)) { throw new BusinessException("生产销售单位不能为空"); } + + if (string.IsNullOrWhiteSpace(input.ProducerCustomsCode)) + { + throw new BusinessException("生产销售单位海关代码不能为空"); + } + + if (string.IsNullOrWhiteSpace(input.CustomsPort)) + { + throw new BusinessException("备案海关港口地不能为空"); + } } private static Expression> BuildPredicate( diff --git a/src/Oms.Application/Services/Orders/Customs9610OrderWriter.cs b/src/Oms.Application/Services/Orders/Customs9610OrderWriter.cs index f0a4f1d..22b9437 100644 --- a/src/Oms.Application/Services/Orders/Customs9610OrderWriter.cs +++ b/src/Oms.Application/Services/Orders/Customs9610OrderWriter.cs @@ -1,4 +1,6 @@ using Fake.DependencyInjection; +using Oms.Domain.CustomersAggregate.Entities; +using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Repositories; @@ -13,15 +15,18 @@ public class Customs9610OrderWriter : IScopedDependency private readonly ICustoms9610OrderRepository _headerRepository; private readonly ICustoms9610OrderItemRepository _itemRepository; private readonly IOrderRepository _orderRepository; + private readonly ICustomerExportQualificationRepository _exportQualificationRepository; public Customs9610OrderWriter( ICustoms9610OrderRepository headerRepository, ICustoms9610OrderItemRepository itemRepository, - IOrderRepository orderRepository) + IOrderRepository orderRepository, + ICustomerExportQualificationRepository exportQualificationRepository) { _headerRepository = headerRepository; _itemRepository = itemRepository; _orderRepository = orderRepository; + _exportQualificationRepository = exportQualificationRepository; } public async Task UpsertFromOrderAsync(Order order, CancellationToken cancellationToken = default) @@ -55,6 +60,7 @@ public class Customs9610OrderWriter : IScopedDependency var grossWeight = ResolveGrossWeight(weights); var serviceNo = order.ServiceNo?.Trim() ?? string.Empty; var clientNo = FirstNonEmpty(order.ClientNo, order.ServiceNo) ?? string.Empty; + var qualification = await ResolveQualificationAsync(order, cancellationToken); if (existing != null && string.Equals(existing.Status, Customs9610StatusCodes.Declared, StringComparison.OrdinalIgnoreCase)) @@ -98,6 +104,8 @@ public class Customs9610OrderWriter : IScopedDependency header.PreRecordFlag = "1"; } + ApplyQualification(header, qualification); + header.GrossWeight = grossWeight; header.Currency = FirstNonEmpty(order.DeclaredCurrency, header.Currency, "CNY")!; header.DestCountry = FirstNonEmpty(order.ConsigneeCountry, header.DestCountry) ?? string.Empty; @@ -141,6 +149,50 @@ public class Customs9610OrderWriter : IScopedDependency } } + private async Task ResolveQualificationAsync( + Order order, + CancellationToken cancellationToken) + { + if (order.CustomerId is not > 0) + { + return null; + } + + if (order.ExportQualificationId is > 0) + { + var owned = await _exportQualificationRepository.GetOwnedAsync( + order.ExportQualificationId.Value, + order.CustomerId.Value, + cancellationToken); + if (owned != null) + { + return owned; + } + } + + return await _exportQualificationRepository.GetDefaultAsync( + order.CustomerId.Value, + cancellationToken); + } + + private static void ApplyQualification(Customs9610Order header, CustomerExportQualification? qualification) + { + if (qualification == null) + { + return; + } + + header.EcomCorpCode = Truncate( + FirstNonEmpty(qualification.DomesticConsignorCustomsCode, qualification.SocialCreditCode), + 18); + header.EcomCorpName = Truncate( + FirstNonEmpty(qualification.CompanyName, qualification.DomesticConsignor), + 100); + header.ProducerCode = Truncate(qualification.ProducerCustomsCode, 18); + header.ProducerName = Truncate(qualification.ProducerName, 100); + header.ActualSellerCode = Truncate(qualification.SocialCreditCode, 18); + } + private static decimal ResolveGrossWeight(IReadOnlyList weights) { var forecast = weights.FirstOrDefault(x => diff --git a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs index 7d1d00b..9444025 100644 --- a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs +++ b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs @@ -200,6 +200,7 @@ public partial class OrderService : null, RequestId = order.RequestId, ExportCustomsMode = order.ExportCustomsMode, + ExportQualificationId = order.ExportQualificationId, PredeclareFailReason = string.IsNullOrWhiteSpace(order.PredeclareFailReason) ? null : order.PredeclareFailReason.Trim(), diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs index fb58dee..ab6f286 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs @@ -40,6 +40,8 @@ public partial class OrderService private static readonly ConcurrentDictionary InboundBillingInFlight = new(); private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService(); + private ICustomerExportQualificationRepository ExportQualificationRepository => + LazyServiceProvider.GetRequiredService(); private IMemberLevelRepository MemberLevelRepository => LazyServiceProvider.GetRequiredService(); @@ -409,6 +411,39 @@ public partial class OrderService throw new BusinessException("出口报关方式不合法"); } + var is9610Summary = string.Equals( + input.ExportCustomsMode, + ExportCustomsModeCodes.Mode9610Summary, + StringComparison.OrdinalIgnoreCase); + if (is9610Summary) + { + CustomerExportQualification? qualification = null; + if (input.ExportQualificationId is > 0) + { + qualification = await ExportQualificationRepository.GetOwnedAsync( + input.ExportQualificationId.Value, + input.CustomerId!.Value); + if (qualification == null) + { + throw new BusinessException("出口资质备案不存在或不属于当前客户"); + } + } + else + { + qualification = await ExportQualificationRepository.GetDefaultAsync(input.CustomerId!.Value); + if (qualification == null) + { + throw new BusinessException("9610汇总申报必须选择出口资质备案(或先维护默认备案)"); + } + + input.ExportQualificationId = qualification.Id; + } + } + else + { + input.ExportQualificationId = null; + } + if (string.IsNullOrWhiteSpace(input.DeclaredCurrency)) { throw new BusinessException("申报币种不能为空"); @@ -1906,6 +1941,7 @@ public partial class OrderService ConsigneeCountryName = consigneeCountryName, CargoAttribute = input.CargoAttribute ?? CargoAttributeCodes.General, ExportCustomsMode = input.ExportCustomsMode, + ExportQualificationId = input.ExportQualificationId, WarehouseId = input.WarehouseId, CustomerId = input.CustomerId, OrderSource = orderSource, diff --git a/src/Oms.Application/Services/Orders/Templates/OrderTemplateFieldRegistry.cs b/src/Oms.Application/Services/Orders/Templates/OrderTemplateFieldRegistry.cs index ba2043d..cb8505c 100644 --- a/src/Oms.Application/Services/Orders/Templates/OrderTemplateFieldRegistry.cs +++ b/src/Oms.Application/Services/Orders/Templates/OrderTemplateFieldRegistry.cs @@ -110,10 +110,11 @@ public static class OrderTemplateFieldRegistry Add("warehouseId", "发货仓库", true, "select", "warehouse", "basic", "1", hint: "从下拉选择发货仓库 ID"); Add("actualWeight", "实重(kg)", true, "number", example: "1.2", hint: "包裹实际重量,单位千克"); Add("quantity", "包裹件数", true, "number", example: "1", hint: "一般为 1"); - Add("length", "长(cm)", false, "number", example: "10", hint: "选填,用于材积计费"); - Add("width", "宽(cm)", false, "number", example: "10", hint: "选填,用于材积计费"); - Add("height", "高(cm)", false, "number", example: "10", hint: "选填,用于材积计费"); - Add("exportCustomsMode", "出口报关", false, "select", "export_customs_mode", "basic", "9610Simple", hint: "不选则按简易申报"); + Add("length", "长(cm)", true, "number", example: "10", hint: "必填,用于材积计费"); + Add("width", "宽(cm)", true, "number", example: "10", hint: "必填,用于材积计费"); + Add("height", "高(cm)", true, "number", example: "10", hint: "必填,用于材积计费"); + Add("exportCustomsMode", "出口报关", false, "select", "export_customs_mode", "basic", "9610Simple", hint: "不选则按简易申报;填 9610Summary 时需有默认出口资质或填备案Id"); + Add("exportQualificationId", "出口资质备案Id", false, "number", example: "1", hint: "9610汇总申报时填写;留空则用客户默认备案"); Add("receiver.recipientsName", "收件人姓名", true, group: "receiver", example: "John Smith", hint: "必填"); Add("receiver.phone", "收件人手机", true, group: "receiver", example: "13900139000", hint: "必填"); @@ -167,11 +168,12 @@ public static class OrderTemplateFieldRegistry Add("clientNo", "客户单号", true, example: "C202604230001", hint: "您自己的订单编号,同一客户不可重复"); Add("productCode", "运输方式", true, "select", "sales_product", "basic", "PROD001", hint: "从下拉选择产品编码"); Add("cargoAttribute", "货物属性", true, "select", "cargo_attribute", "basic", "general", hint: "普货选 general"); - Add("exportCustomsMode", "出口报关方式", false, "select", "export_customs_mode", "basic", "9610Simple", hint: "不选则简易申报"); + Add("exportCustomsMode", "出口报关方式", false, "select", "export_customs_mode", "basic", "9610Simple", hint: "不选则简易申报;填 9610Summary 时需有默认出口资质或填备案Id"); + Add("exportQualificationId", "出口资质备案Id", false, "number", example: "1", hint: "9610汇总申报时填写;留空则用客户默认备案"); Add("warehouseId", "发货仓库", true, "select", "warehouse", "basic", "1", hint: "从下拉选择仓库 ID"); - Add("length", "长(cm)", false, "number", example: "10", hint: "材积计费用"); - Add("width", "宽(cm)", false, "number", example: "10", hint: "材积计费用"); - Add("height", "高(cm)", false, "number", example: "10", hint: "材积计费用"); + Add("length", "长(cm)", true, "number", example: "10", hint: "必填,材积计费用"); + Add("width", "宽(cm)", true, "number", example: "10", hint: "必填,材积计费用"); + Add("height", "高(cm)", true, "number", example: "10", hint: "必填,材积计费用"); Add("actualWeight", "实重(kg)", true, "number", example: "1.2", hint: "必填"); Add("isCod", "是否COD", false, "boolean", example: "false", hint: "货到付款填 true"); Add("codCurrency", "COD币种", false, "select", "currency", example: "USD", hint: "COD 订单填写"); diff --git a/src/Oms.Application/Services/Orders/Templates/OrderTemplateHeaderMatcher.cs b/src/Oms.Application/Services/Orders/Templates/OrderTemplateHeaderMatcher.cs index 76830e7..7ba39ba 100644 --- a/src/Oms.Application/Services/Orders/Templates/OrderTemplateHeaderMatcher.cs +++ b/src/Oms.Application/Services/Orders/Templates/OrderTemplateHeaderMatcher.cs @@ -20,6 +20,10 @@ public static class OrderTemplateHeaderMatcher ["产品编码"] = "productCode", ["货物属性"] = "cargoAttribute", ["出口报关方式"] = "exportCustomsMode", + ["出口报关"] = "exportCustomsMode", + ["出口资质备案Id"] = "exportQualificationId", + ["出口资质备案"] = "exportQualificationId", + ["资质备案Id"] = "exportQualificationId", ["仓库"] = "warehouseId", ["仓库ID"] = "warehouseId", ["长cm"] = "length", diff --git a/src/Oms.Application/Services/Orders/Templates/OrderTemplateRowMapper.cs b/src/Oms.Application/Services/Orders/Templates/OrderTemplateRowMapper.cs index e9146fb..b1470a6 100644 --- a/src/Oms.Application/Services/Orders/Templates/OrderTemplateRowMapper.cs +++ b/src/Oms.Application/Services/Orders/Templates/OrderTemplateRowMapper.cs @@ -27,7 +27,7 @@ public static class OrderTemplateRowMapper private static readonly HashSet ForceNumberFields = new(StringComparer.OrdinalIgnoreCase) { - "warehouseId", "length", "width", "height", "actualWeight", "codSum", "declaredSum", "quantity", + "warehouseId", "exportQualificationId", "length", "width", "height", "actualWeight", "codSum", "declaredSum", "quantity", "receiver.taxType", "receiver.certificateType", "items[0].unitPrice", "items[0].unitWeight", "items[0].quantity", "items[0].amount", "items[0].weight" }; @@ -75,9 +75,28 @@ public static class OrderTemplateRowMapper } ApplyDerivedDeclarationFields(input); + ApplyParcelDimensionDefaults(input); return input; } + private static void ApplyParcelDimensionDefaults(OrderParcelCreateInput input) + { + if (!input.Length.HasValue || input.Length <= 0) + { + input.Length = 10; + } + + if (!input.Width.HasValue || input.Width <= 0) + { + input.Width = 10; + } + + if (!input.Height.HasValue || input.Height <= 0) + { + input.Height = 10; + } + } + private static void ApplyDerivedDeclarationFields(OrderParcelCreateInput input) { if (string.IsNullOrWhiteSpace(input.DeclaredCurrency)) diff --git a/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerExportQualificationRepository.cs b/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerExportQualificationRepository.cs index 2745c10..a017633 100644 --- a/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerExportQualificationRepository.cs +++ b/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerExportQualificationRepository.cs @@ -10,6 +10,10 @@ public interface ICustomerExportQualificationRepository : IRepository GetDefaultAsync( + int customerId, + CancellationToken cancellationToken = default); + Task ClearDefaultAsync( int customerId, int? excludeId = null, diff --git a/src/Oms.Domain/OrdersAggregate/Entities/Order.cs b/src/Oms.Domain/OrdersAggregate/Entities/Order.cs index f58d638..611dd00 100644 --- a/src/Oms.Domain/OrdersAggregate/Entities/Order.cs +++ b/src/Oms.Domain/OrdersAggregate/Entities/Order.cs @@ -52,6 +52,12 @@ public class Order : OmsAuditedAggregateRoot public string? ExportCustomsMode { get; set; } + /// + /// 出口资质备案 Id(9610 汇总申报选用)。 + /// + [SugarColumn(IsNullable = true)] + public int? ExportQualificationId { get; set; } + public string? PredeclareFailReason { get; set; } /// diff --git a/src/Oms.SqlSugarCore/Customers/CustomerExportQualificationRepository.cs b/src/Oms.SqlSugarCore/Customers/CustomerExportQualificationRepository.cs index 6852a27..f4886cc 100644 --- a/src/Oms.SqlSugarCore/Customers/CustomerExportQualificationRepository.cs +++ b/src/Oms.SqlSugarCore/Customers/CustomerExportQualificationRepository.cs @@ -18,6 +18,13 @@ public class CustomerExportQualificationRepository return FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId, cancellationToken); } + public Task GetDefaultAsync( + int customerId, + CancellationToken cancellationToken = default) + { + return FirstOrDefaultAsync(x => x.CustomerId == customerId && x.IsDefault == 1, cancellationToken); + } + public async Task ClearDefaultAsync( int customerId, int? excludeId = null,