From 87f4cebc9b38f1f4d358028ae41ef7d8ffc088ca Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Wed, 30 Sep 2026 12:17:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20openApi=20=E7=BB=9F=E4=B8=80=E4=BF=A1?= =?UTF-8?q?=E5=B0=81=E4=B8=8E=E6=8E=A8=E8=B4=B9=E5=AD=97=E6=AE=B5=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=EF=BC=8C=E4=B8=8B=E5=8D=95=E9=89=B4=E6=9D=83=E6=94=B9?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E5=A4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 推费入参对齐表字段;创建订单凭证走 appKey/appSecret Header,去掉上门揽收与 Body 内 customerId。 Co-authored-by: Cursor --- .../Dtos/OpenApi/OpenApiEnvelope.cs | 29 ++++ .../Dtos/Orders/OpenApiParcelCreateInput.cs | 148 +++++++++++++++++- .../Dtos/Orders/PickupOrderDtos.cs | 37 +++-- .../Services/OpenApi/OpenApiService.cs | 59 +++++-- .../Orders/PickupOrderFeePushService.cs | 30 ++-- .../Shared/CustomerOpenApiAuthHelper.cs | 35 ++++- 6 files changed, 279 insertions(+), 59 deletions(-) create mode 100644 src/Oms.Application/Dtos/OpenApi/OpenApiEnvelope.cs diff --git a/src/Oms.Application/Dtos/OpenApi/OpenApiEnvelope.cs b/src/Oms.Application/Dtos/OpenApi/OpenApiEnvelope.cs new file mode 100644 index 0000000..4d724f0 --- /dev/null +++ b/src/Oms.Application/Dtos/OpenApi/OpenApiEnvelope.cs @@ -0,0 +1,29 @@ +namespace Oms.Application.Dtos.OpenApi; + +/// +/// 对外开放接口统一响应。 +/// +public class OpenApiEnvelope +{ + public string Code { get; set; } = "200"; + + public string Msg { get; set; } = "success"; + + public T? Data { get; set; } + + public static OpenApiEnvelope Ok(T data) => + new() + { + Code = "200", + Msg = "success", + Data = data + }; + + public static OpenApiEnvelope Fail(string msg, string code = "400") => + new() + { + Code = code, + Msg = msg, + Data = default + }; +} diff --git a/src/Oms.Application/Dtos/Orders/OpenApiParcelCreateInput.cs b/src/Oms.Application/Dtos/Orders/OpenApiParcelCreateInput.cs index c48882c..ccf50e9 100644 --- a/src/Oms.Application/Dtos/Orders/OpenApiParcelCreateInput.cs +++ b/src/Oms.Application/Dtos/Orders/OpenApiParcelCreateInput.cs @@ -1,17 +1,149 @@ +using Oms.Domain.OrdersAggregate.Consts; + namespace Oms.Application.Dtos.Orders; /// -/// 开放接口创建订单包裹:在标准下单入参上增加 AppKey/AppSecret(Body,非自定义 Header)。 +/// 开放接口创建订单包裹入参:与单票字段对齐(不含上门揽收)。 +/// 鉴权凭证放请求头 appKey / appSecret,勿放入 Body。 /// -public class OpenApiParcelCreateInput : OrderParcelCreateInput +public class OpenApiParcelCreateInput { - /// - /// 客户开放接口 AppKey。 - /// - public string? AppKey { get; set; } + /// 请求标识(幂等,可选)。 + public string? RequestId { get; set; } + + /// 客户单号。 + public string? ClientNo { get; set; } + + /// 跟踪号(可选,系统通常忽略由下游回写)。 + public string? TrackNo { get; set; } + + /// 服务单号(可选,不传时系统自动生成)。 + public string? ServiceNo { get; set; } + + /// 产品编码(运输方式)。 + public string? ProductCode { get; set; } + + /// 渠道编码(可选)。 + public string? ChannelCode { get; set; } + + /// 货物属性(字典 cargo_attribute:general / electronics / sensitive)。 + public string? CargoAttribute { get; set; } + + /// 出口报关方式(字典 export_customs_mode)。 + public string? ExportCustomsMode { get; set; } + + /// 出口资质备案 Id(9610 汇总申报时)。 + public int? ExportQualificationId { get; set; } + + /// 电商平台(9610 汇总申报时)。 + public string? PlatformCode { get; set; } + + /// 仓库主键。 + public int? WarehouseId { get; set; } + + /// 长(cm)。 + public decimal? Length { get; set; } + + /// 宽(cm)。 + public decimal? Width { get; set; } + + /// 高(cm)。 + public decimal? Height { get; set; } + + /// 实重(kg)。 + public decimal? ActualWeight { get; set; } + + /// 体积重(kg,可选)。 + public decimal? VolumetricWeight { get; set; } + + /// 计费重(kg,可选)。 + public decimal? ChargeableWeight { get; set; } + + /// 体积(cm³,可选)。 + public decimal? Cubic { get; set; } + + /// 是否 COD。 + public bool IsCod { get; set; } + + /// COD 币种。 + public string? CodCurrency { get; set; } + + /// COD 金额。 + public decimal? CodSum { get; set; } + + /// 申报币种(字典 currency)。 + public string? DeclaredCurrency { get; set; } + + /// 申报总金额(可选,不填可由明细汇总)。 + public decimal? DeclaredSum { get; set; } + + /// 附加服务字典 code 列表。 + public List? AdditionalServices { get; set; } + + /// 是否偏远(可选预传)。 + public byte? IsRemote { get; set; } + + /// 包裹数量。 + public int? Quantity { get; set; } + + /// 收件人。 + public OrderParcelReceiverInput? Receiver { get; set; } + + /// 寄件人。 + public OrderParcelSenderInput? Sender { get; set; } + + /// 申报明细。 + public List Items { get; set; } = []; + + /// 优惠券实例 Id(可选)。 + public int? CouponInstanceId { get; set; } + + /// 仅保存草稿,不预报计费。 + public bool SaveAsDraft { get; set; } /// - /// 客户开放接口 AppSecret。 + /// 映射为内部单票创建入参。开放接口固定自发货(selfShip),客户 Id 由凭证解析后写入。 /// - public string? AppSecret { get; set; } + public OrderParcelCreateInput ToOrderParcelCreateInput(int customerId) => + new() + { + RequestId = RequestId, + ClientNo = ClientNo, + TrackNo = TrackNo, + ServiceNo = ServiceNo, + ProductCode = ProductCode, + ChannelCode = ChannelCode, + CargoAttribute = CargoAttribute, + ExportCustomsMode = ExportCustomsMode, + ExportQualificationId = ExportQualificationId, + PlatformCode = PlatformCode, + WarehouseId = WarehouseId, + CustomerId = customerId, + Length = Length, + Width = Width, + Height = Height, + ActualWeight = ActualWeight, + VolumetricWeight = VolumetricWeight, + ChargeableWeight = ChargeableWeight, + Cubic = Cubic, + IsCod = IsCod, + CodCurrency = CodCurrency, + CodSum = CodSum, + DeclaredCurrency = DeclaredCurrency, + DeclaredSum = DeclaredSum, + AdditionalServices = AdditionalServices, + IsRemote = IsRemote, + Quantity = Quantity, + Receiver = Receiver, + Sender = Sender, + Items = Items ?? [], + CouponInstanceId = CouponInstanceId, + SaveAsDraft = SaveAsDraft, + InboundMethod = OrderInboundMethodCodes.SelfShip, + SenderAddressId = null, + ExpectPickupTime = null, + PickupDirection = null, + CarrierCode = null, + CarrierService = null + }; } diff --git a/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs b/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs index ab046bc..d9fd2fd 100644 --- a/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs +++ b/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs @@ -344,10 +344,6 @@ public class PickupFeeLineOutput public class PickupCarrierQuoteOutput { - public string? ServiceMode { get; set; } - - public string? PayMode { get; set; } - public List ValueInfoList { get; set; } = []; } @@ -364,7 +360,7 @@ public class PickupOpenApiPushFeeResult { public string PickupNo { get; set; } = string.Empty; - public decimal ActualWeight { get; set; } + public decimal TotalActualWeight { get; set; } public decimal ChargeableWeight { get; set; } @@ -372,31 +368,34 @@ public class PickupOpenApiPushFeeResult public decimal ReceivableAmount { get; set; } - public decimal PayableAmount { get; set; } + public decimal? CarrierPayableAmount { get; set; } public decimal? CarrierQuotedFreightAmount { get; set; } } +/// +/// 承运推送件重尺入参(字段名对齐 t_pickup_order)。 +/// public class PickupOpenApiPushFeeInput { - /// - /// 承运运单号,对应库字段 CarrierWaybillNo。 - /// - public string? WaybillNumber { get; set; } + /// 承运运单号。 + public string? CarrierWaybillNo { get; set; } - public decimal ActualWeight { get; set; } + /// 实重合计。 + public decimal TotalActualWeight { get; set; } - public decimal? FreightWeight { get; set; } + /// 承运计费重(对照)。 + public decimal? CarrierFreightWeight { get; set; } - public decimal? TotalFreightAmount { get; set; } + /// 承运报价运费合计(对照)。 + public decimal? CarrierQuotedFreightAmount { get; set; } - public decimal? ReceivableAmount { get; set; } + /// 承运报文应收(对照)。 + public decimal? CarrierQuotedReceivableAmount { get; set; } + /// 承运附加费明细(写入 CarrierQuoteJson)。 public List? ValueInfoList { get; set; } - public List? GoodsSizeList { get; set; } - - public string? ServiceMode { get; set; } - - public string? PayMode { get; set; } + /// 尺寸明细(写入 PackageSizesJson)。 + public List? PackageSizes { get; set; } } diff --git a/src/Oms.Application/Services/OpenApi/OpenApiService.cs b/src/Oms.Application/Services/OpenApi/OpenApiService.cs index 189b4ef..2b5c66f 100644 --- a/src/Oms.Application/Services/OpenApi/OpenApiService.cs +++ b/src/Oms.Application/Services/OpenApi/OpenApiService.cs @@ -1,6 +1,7 @@ using Fake.Application; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Oms.Application.Dtos.OpenApi; using Oms.Application.Dtos.Orders; using Oms.Application.Services.Orders; using Oms.Application.Shared; @@ -9,7 +10,8 @@ namespace Oms.Application.Services.OpenApi; /// /// 对外开放 API(Swagger 文档分组:openApi)。 -/// 客户机机:AppKey/AppSecret 放在 JSON Body;供应商推费由上游 itfx 鉴权,OMS 不再验 key。 +/// 统一响应:{ code, msg, data }。 +/// 客户机机鉴权:请求头 appKey + appSecret(勿放 Body,避免落日志)。 /// [ApiExplorerSettings(GroupName = "openApi")] public class OpenApiService : ApplicationService @@ -24,29 +26,58 @@ public class OpenApiService : ApplicationService LazyServiceProvider.GetRequiredService(); /// - /// 推送揽收件重尺并按当前产品计算应收应付(上游 itfx 鉴权后转发,无需 AppKey)。 + /// 推送揽收件重尺并按当前产品计算应收应付(上游 itfx 鉴权后转发,无需客户凭证)。 /// [AllowAnonymous] [HttpPost("/openapi/pickup-order/push-fee")] - public Task PushFeeAsync([FromBody] PickupOpenApiPushFeeInput input) => - FeePush.PushFeeAsync(input); + public async Task> PushFeeAsync( + [FromBody] PickupOpenApiPushFeeInput input) + { + try + { + var data = await FeePush.PushFeeAsync(input); + return OpenApiEnvelope.Ok(data); + } + catch (BusinessException ex) + { + return OpenApiEnvelope.Fail(ex.Message); + } + catch (Exception ex) + { + return OpenApiEnvelope.Fail( + string.IsNullOrWhiteSpace(ex.Message) ? "系统异常" : ex.Message, + "500"); + } + } /// - /// 创建订单包裹(客户机机:Body 传 AppKey、AppSecret)。 + /// 创建订单包裹(仅自发货)。 + /// 鉴权:Header appKey、appSecret;客户归属强制取自凭证,Body 不可指定 customerId。 /// [AllowAnonymous] [HttpPost("/openapi/parcel/create")] - public async Task CreateParcelAsync([FromBody] OpenApiParcelCreateInput input) + public async Task> CreateParcelAsync( + [FromHeader(Name = CustomerOpenApiAuthHelper.AppKeyHeaderName)] string? appKey, + [FromHeader(Name = CustomerOpenApiAuthHelper.AppSecretHeaderName)] string? appSecret, + [FromBody] OpenApiParcelCreateInput input) { - ArgumentNullException.ThrowIfNull(input); - - var (customer, _) = await AuthHelper.ResolveAsync(input.AppKey, input.AppSecret); - if (input.CustomerId is > 0 && input.CustomerId.Value != customer.Id) + try { - throw new BusinessException("凭证无效"); - } + ArgumentNullException.ThrowIfNull(input); - input.CustomerId = customer.Id; - return await OrderService.CreateParcelAsync(input); + var (customer, _) = await AuthHelper.ResolveAsync(appKey, appSecret); + var data = await OrderService.CreateParcelAsync(input.ToOrderParcelCreateInput(customer.Id)); + return OpenApiEnvelope.Ok(data); + } + catch (BusinessException ex) + { + return OpenApiEnvelope.Fail(ex.Message); + } + catch (Exception ex) + { + return OpenApiEnvelope.Fail( + string.IsNullOrWhiteSpace(ex.Message) ? "系统异常" : ex.Message, + "500"); + } } } diff --git a/src/Oms.Application/Services/Orders/PickupOrderFeePushService.cs b/src/Oms.Application/Services/Orders/PickupOrderFeePushService.cs index 37fcafc..7cb00fd 100644 --- a/src/Oms.Application/Services/Orders/PickupOrderFeePushService.cs +++ b/src/Oms.Application/Services/Orders/PickupOrderFeePushService.cs @@ -51,21 +51,21 @@ public class PickupOrderFeePushService : IScopedDependency throw new BusinessException("请求体不能为空"); } - var carrierWaybillNo = (input.WaybillNumber ?? string.Empty).Trim(); + var carrierWaybillNo = (input.CarrierWaybillNo ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(carrierWaybillNo)) { - throw new BusinessException("waybillNumber(承运运单号 CarrierWaybillNo)不能为空"); + throw new BusinessException("carrierWaybillNo(承运运单号)不能为空"); } - if (input.ActualWeight <= 0) + if (input.TotalActualWeight <= 0) { - throw new BusinessException("actualWeight 必须大于 0"); + throw new BusinessException("totalActualWeight 必须大于 0"); } - var sizes = NormalizeSizes(input.GoodsSizeList); + var sizes = NormalizeSizes(input.PackageSizes); if (sizes.Count == 0) { - throw new BusinessException("goodsSizeList 不能为空"); + throw new BusinessException("packageSizes 不能为空"); } var pickup = await _pickupOrderRepository.GetByCarrierWaybillNoAsync(carrierWaybillNo) @@ -97,7 +97,7 @@ public class PickupOrderFeePushService : IScopedDependency Length = totalVolume, Width = 1, Height = 1, - ActualWeight = input.ActualWeight, + ActualWeight = input.TotalActualWeight, Cubic = ParcelBillingCalculator.CalculateCubic(totalVolume, 1, 1) }; @@ -184,17 +184,15 @@ public class PickupOrderFeePushService : IScopedDependency var freight = receivableFees.FirstOrDefault()?.Amount ?? receivableTotal; var other = Math.Max(0, receivableTotal - freight); - pickup.TotalActualWeight = input.ActualWeight; + pickup.TotalActualWeight = input.TotalActualWeight; pickup.VolumetricWeight = volumetricWeight; pickup.ChargeableWeight = chargeableWeight; pickup.PackageSizesJson = OmsDocHelper.SerializeJson(sizes); - pickup.CarrierFreightWeight = input.FreightWeight; - pickup.CarrierQuotedFreightAmount = input.TotalFreightAmount; - pickup.CarrierQuotedReceivableAmount = input.ReceivableAmount; + pickup.CarrierFreightWeight = input.CarrierFreightWeight; + pickup.CarrierQuotedFreightAmount = input.CarrierQuotedFreightAmount; + pickup.CarrierQuotedReceivableAmount = input.CarrierQuotedReceivableAmount; pickup.CarrierQuoteJson = OmsDocHelper.SerializeJson(new PickupCarrierQuoteOutput { - ServiceMode = input.ServiceMode, - PayMode = input.PayMode, ValueInfoList = input.ValueInfoList ?? [] }); pickup.ReceivableFeeJson = OmsDocHelper.SerializeJson(receivableFees); @@ -210,12 +208,12 @@ public class PickupOrderFeePushService : IScopedDependency return new PickupOpenApiPushFeeResult { PickupNo = pickup.PickupNo, - ActualWeight = input.ActualWeight, + TotalActualWeight = input.TotalActualWeight, ChargeableWeight = chargeableWeight, VolumetricWeight = volumetricWeight, ReceivableAmount = receivableTotal, - PayableAmount = payableTotal, - CarrierQuotedFreightAmount = input.TotalFreightAmount + CarrierPayableAmount = payableTotal, + CarrierQuotedFreightAmount = input.CarrierQuotedFreightAmount }; } diff --git a/src/Oms.Application/Shared/CustomerOpenApiAuthHelper.cs b/src/Oms.Application/Shared/CustomerOpenApiAuthHelper.cs index 94e7458..b65658c 100644 --- a/src/Oms.Application/Shared/CustomerOpenApiAuthHelper.cs +++ b/src/Oms.Application/Shared/CustomerOpenApiAuthHelper.cs @@ -1,3 +1,5 @@ +using System.Security.Cryptography; +using System.Text; using Fake.Application; using Fake.DependencyInjection; using Microsoft.AspNetCore.Http; @@ -8,10 +10,13 @@ using Oms.Domain.CustomersAggregate.Repositories; namespace Oms.Application.Shared; /// -/// 客户 OpenAPI 机机鉴权:按 AppKey+AppSecret 解析主客户,供 whoami 及后续业务接口复用。 +/// 客户 OpenAPI 机机鉴权:按请求头 AppKey + AppSecret 解析主客户。 /// public class CustomerOpenApiAuthHelper : IScopedDependency { + public const string AppKeyHeaderName = "appKey"; + public const string AppSecretHeaderName = "appSecret"; + private readonly ICustomerOpenApiCredentialRepository _credentialRepository; private readonly ICustomerRepository _customerRepository; private readonly IHttpContextAccessor _httpContextAccessor; @@ -26,6 +31,19 @@ public class CustomerOpenApiAuthHelper : IScopedDependency _httpContextAccessor = httpContextAccessor; } + /// + /// 从当前请求头读取 appKey / appSecret 并鉴权。 + /// + public Task<(Customer Customer, CustomerOpenApiCredential Credential)> ResolveFromHeadersAsync( + CancellationToken cancellationToken = default) + { + var http = _httpContextAccessor.HttpContext + ?? throw new BusinessException("凭证无效"); + var appKey = http.Request.Headers[AppKeyHeaderName].FirstOrDefault(); + var appSecret = http.Request.Headers[AppSecretHeaderName].FirstOrDefault(); + return ResolveAsync(appKey, appSecret, cancellationToken); + } + public async Task<(Customer Customer, CustomerOpenApiCredential Credential)> ResolveAsync( string? appKey, string? appSecret, @@ -41,7 +59,7 @@ public class CustomerOpenApiAuthHelper : IScopedDependency var credential = await _credentialRepository.GetByAppKeyAsync(key, cancellationToken); if (credential == null || !credential.Enabled - || !string.Equals(credential.AppSecret, secret, StringComparison.Ordinal)) + || !FixedTimeEqualsUtf8(credential.AppSecret, secret)) { throw new BusinessException("凭证无效"); } @@ -63,6 +81,19 @@ public class CustomerOpenApiAuthHelper : IScopedDependency return (customer, credential); } + private static bool FixedTimeEqualsUtf8(string? expected, string actual) + { + if (string.IsNullOrEmpty(expected)) + { + return false; + } + + var left = Encoding.UTF8.GetBytes(expected); + var right = Encoding.UTF8.GetBytes(actual); + return left.Length == right.Length + && CryptographicOperations.FixedTimeEquals(left, right); + } + private string? ResolveClientIp() { var http = _httpContextAccessor.HttpContext;