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;