feat: openApi 分组暴露推费与下单,去掉 OMS 侧 appKey 校验
All checks were successful
Build and Deploy / deploy (push) Successful in 58s

Swagger openApi 提供 /openapi/pickup-order/push-fee 与 /openapi/parcel/create;删除 method 路由与 whoami。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-09-30 10:36:24 +08:00
parent 2688058cb3
commit a3b54b6b07
9 changed files with 73 additions and 260 deletions

View File

@ -41,26 +41,3 @@ public class CustomerOpenApiCredentialAdminSetEnabledInput : CustomerOpenApiCred
{
public bool Enabled { get; set; }
}
public class CustomerOpenApiWhoAmIInput
{
public string? AppSecret { get; set; }
}
public class CustomerOpenApiWhoAmIResult
{
public int CustomerId { get; set; }
public string CustomerCode { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
}
public class CustomerOpenApiEnvelope
{
public string Code { get; set; } = "200";
public string Msg { get; set; } = "成功";
public CustomerOpenApiWhoAmIResult? Data { get; set; }
}

View File

@ -0,0 +1,17 @@
namespace Oms.Application.Dtos.Orders;
/// <summary>
/// 开放接口创建订单包裹:在标准下单入参上增加 AppKey/AppSecret(Body,非自定义 Header)。
/// </summary>
public class OpenApiParcelCreateInput : OrderParcelCreateInput
{
/// <summary>
/// 客户开放接口 AppKey。
/// </summary>
public string? AppKey { get; set; }
/// <summary>
/// 客户开放接口 AppSecret。
/// </summary>
public string? AppSecret { get; set; }
}

View File

@ -360,15 +360,6 @@ public class PickupCarrierValueFeeOutput
public decimal? FeeAmount { get; set; }
}
public class PickupOpenApiEnvelope
{
public string Code { get; set; } = "200";
public string Msg { get; set; } = "成功";
public PickupOpenApiPushFeeResult? Data { get; set; }
}
public class PickupOpenApiPushFeeResult
{
public string PickupNo { get; set; } = string.Empty;

View File

@ -1,63 +0,0 @@
using Fake.Application;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Customers;
using Oms.Application.Shared;
namespace Oms.Application.Services.Customers;
/// <summary>
/// 客户 OpenAPI 机机入口(AllowAnonymous,AppKey + AppSecret)。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Customer")]
public class CustomerOpenApiService : ApplicationService
{
private CustomerOpenApiAuthHelper AuthHelper =>
LazyServiceProvider.GetRequiredService<CustomerOpenApiAuthHelper>();
/// <summary>
/// 探针:校验凭证并返回客户身份。
/// </summary>
[AllowAnonymous]
[HttpPost("/openapi/customer/whoami")]
public async Task<CustomerOpenApiEnvelope> WhoAmIAsync(
[FromQuery] string? appKey,
[FromHeader(Name = "appSecret")] string? appSecretHeader,
[FromBody] CustomerOpenApiWhoAmIInput? body)
{
try
{
var appSecret = !string.IsNullOrWhiteSpace(appSecretHeader)
? appSecretHeader
: body?.AppSecret;
var (customer, _) = await AuthHelper.ResolveAsync(appKey, appSecret);
return new CustomerOpenApiEnvelope
{
Code = "200",
Msg = "成功",
Data = new CustomerOpenApiWhoAmIResult
{
CustomerId = customer.Id,
CustomerCode = customer.CustomerCode,
CustomerName = customer.CustomerName
}
};
}
catch (BusinessException ex)
{
return Fail(ex.Message);
}
catch (Exception ex)
{
return Fail(string.IsNullOrWhiteSpace(ex.Message) ? "系统异常" : ex.Message, "500");
}
}
private static CustomerOpenApiEnvelope Fail(string msg, string code = "401") =>
new()
{
Code = code,
Msg = msg,
Data = null
};
}

View File

@ -0,0 +1,52 @@
using Fake.Application;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Orders;
using Oms.Application.Services.Orders;
using Oms.Application.Shared;
namespace Oms.Application.Services.OpenApi;
/// <summary>
/// 对外开放 API(Swagger 文档分组:openApi)。
/// 客户机机:AppKey/AppSecret 放在 JSON Body;供应商推费由上游 itfx 鉴权,OMS 不再验 key。
/// </summary>
[ApiExplorerSettings(GroupName = "openApi")]
public class OpenApiService : ApplicationService
{
private CustomerOpenApiAuthHelper AuthHelper =>
LazyServiceProvider.GetRequiredService<CustomerOpenApiAuthHelper>();
private PickupOrderFeePushService FeePush =>
LazyServiceProvider.GetRequiredService<PickupOrderFeePushService>();
private OrderService OrderService =>
LazyServiceProvider.GetRequiredService<OrderService>();
/// <summary>
/// 推送揽收件重尺并按当前产品计算应收应付(上游 itfx 鉴权后转发,无需 AppKey)。
/// </summary>
[AllowAnonymous]
[HttpPost("/openapi/pickup-order/push-fee")]
public Task<PickupOpenApiPushFeeResult> PushFeeAsync([FromBody] PickupOpenApiPushFeeInput input) =>
FeePush.PushFeeAsync(input);
/// <summary>
/// 创建订单包裹(客户机机:Body 传 AppKey、AppSecret)。
/// </summary>
[AllowAnonymous]
[HttpPost("/openapi/parcel/create")]
public async Task<OrderParcelCreateOutput> CreateParcelAsync([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)
{
throw new BusinessException("凭证无效");
}
input.CustomerId = customer.Id;
return await OrderService.CreateParcelAsync(input);
}
}

View File

@ -1,76 +0,0 @@
using Fake.Application;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Orders;
namespace Oms.Application.Services.Orders;
/// <summary>
/// 国内揽收开放接口(承运商推送件重尺)。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Order")]
public class PickupOpenApiService : ApplicationService
{
private PickupOrderFeePushService FeePush =>
LazyServiceProvider.GetRequiredService<PickupOrderFeePushService>();
/// <summary>
/// 开放平台路由:method=open.api.openCommon.pushFee,仅校验 appKey。
/// </summary>
[AllowAnonymous]
[HttpPost("/openapi")]
public Task<PickupOpenApiEnvelope> InvokeAsync(
[FromQuery] string? appKey,
[FromQuery] string? method,
[FromBody] PickupOpenApiPushFeeInput? body) =>
HandlePushFeeAsync(appKey, method, body);
/// <summary>
/// 推送揽收重量/尺寸并按当前产品计算应收应付。
/// </summary>
[AllowAnonymous]
[HttpPost("/openapi/openCommon/pushFee")]
public Task<PickupOpenApiEnvelope> PushFeeAsync(
[FromQuery] string? appKey,
[FromBody] PickupOpenApiPushFeeInput? body) =>
HandlePushFeeAsync(appKey, PickupOrderFeePushService.PushFeeMethod, body);
private async Task<PickupOpenApiEnvelope> HandlePushFeeAsync(
string? appKey,
string? method,
PickupOpenApiPushFeeInput? body)
{
try
{
if (!string.IsNullOrWhiteSpace(method)
&& !string.Equals(method.Trim(), PickupOrderFeePushService.PushFeeMethod, StringComparison.OrdinalIgnoreCase))
{
return Fail("400", $"不支持的 method:{method}");
}
var data = await FeePush.PushFeeAsync(appKey, body);
return new PickupOpenApiEnvelope
{
Code = "200",
Msg = "成功",
Data = data
};
}
catch (BusinessException ex)
{
return Fail("400", ex.Message);
}
catch (Exception ex)
{
return Fail("500", string.IsNullOrWhiteSpace(ex.Message) ? "系统异常" : ex.Message);
}
}
private static PickupOpenApiEnvelope Fail(string code, string msg) =>
new()
{
Code = code,
Msg = msg,
Data = null
};
}

View File

@ -2,7 +2,6 @@ using Fake.Application;
using Fake.DependencyInjection;
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Application.Integrations.Itfx;
using Oms.Application.Services.Orders.Issues;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Entities;
@ -10,7 +9,6 @@ using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.SystemAggregate.Consts;
using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.Orders;
@ -20,51 +18,34 @@ namespace Oms.Application.Services.Orders;
/// </summary>
public class PickupOrderFeePushService : IScopedDependency
{
public const string PushFeeMethod = "open.api.openCommon.pushFee";
private readonly IPickupOrderRepository _pickupOrderRepository;
private readonly ISystemOtherConfigRepository _otherConfigRepository;
private readonly ISalesProductRepository _salesProductRepository;
private readonly IServiceChannelRepository _serviceChannelRepository;
private readonly IServiceChannelGroupRepository _serviceChannelGroupRepository;
private readonly IWeightRuleRepository _weightRuleRepository;
private readonly IWeightRoundingRepository _weightRoundingRepository;
private readonly IKpiAreaRepository _kpiAreaRepository;
private readonly IWarehouseAddressRepository _warehouseAddressRepository;
private readonly IItfxMetadataClient _itfxMetadataClient;
public PickupOrderFeePushService(
IPickupOrderRepository pickupOrderRepository,
ISystemOtherConfigRepository otherConfigRepository,
ISalesProductRepository salesProductRepository,
IServiceChannelRepository serviceChannelRepository,
IServiceChannelGroupRepository serviceChannelGroupRepository,
IWeightRuleRepository weightRuleRepository,
IWeightRoundingRepository weightRoundingRepository,
IKpiAreaRepository kpiAreaRepository,
IWarehouseAddressRepository warehouseAddressRepository,
IItfxMetadataClient itfxMetadataClient)
IWarehouseAddressRepository warehouseAddressRepository)
{
_pickupOrderRepository = pickupOrderRepository;
_otherConfigRepository = otherConfigRepository;
_salesProductRepository = salesProductRepository;
_serviceChannelRepository = serviceChannelRepository;
_serviceChannelGroupRepository = serviceChannelGroupRepository;
_weightRuleRepository = weightRuleRepository;
_weightRoundingRepository = weightRoundingRepository;
_kpiAreaRepository = kpiAreaRepository;
_warehouseAddressRepository = warehouseAddressRepository;
_itfxMetadataClient = itfxMetadataClient;
}
public async Task<PickupOpenApiPushFeeResult> PushFeeAsync(string? appKey, PickupOpenApiPushFeeInput? input)
public async Task<PickupOpenApiPushFeeResult> PushFeeAsync(PickupOpenApiPushFeeInput? input)
{
var key = (appKey ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(key))
{
throw new BusinessException("appKey 不能为空");
}
if (input == null)
{
throw new BusinessException("请求体不能为空");
@ -90,8 +71,6 @@ public class PickupOrderFeePushService : IScopedDependency
var pickup = await _pickupOrderRepository.GetByCarrierWaybillNoAsync(carrierWaybillNo)
?? throw new BusinessException($"揽收单不存在:承运运单号 {carrierWaybillNo}");
await EnsureAppKeyAsync(key, pickup);
if (pickup.PayStatus == PickupPayStatusCodes.Paid)
{
throw new BusinessException("揽收单已支付,拒绝修改件重尺与费用");
@ -240,67 +219,6 @@ public class PickupOrderFeePushService : IScopedDependency
};
}
private async Task EnsureAppKeyAsync(string appKey, PickupOrder pickup)
{
var config = await _otherConfigRepository.GetByConfigCodeAsync(SystemOtherConfigCodes.PickupOpenApi);
if (config != null && config.Enabled && !string.IsNullOrWhiteSpace(config.AppKey))
{
if (!string.Equals(config.AppKey.Trim(), appKey, StringComparison.Ordinal))
{
throw new BusinessException("appKey 不正确");
}
return;
}
var expected = await ResolvePickupAppKeyAsync(pickup);
if (string.IsNullOrWhiteSpace(expected)
|| !string.Equals(expected, appKey, StringComparison.Ordinal))
{
throw new BusinessException("appKey 不正确");
}
}
private async Task<string?> ResolvePickupAppKeyAsync(PickupOrder pickup)
{
if (string.IsNullOrWhiteSpace(pickup.CarrierService))
{
return null;
}
var product = await _salesProductRepository.GetByProductCodeAsync(pickup.CarrierService.Trim());
if (product == null)
{
return null;
}
var providers = await _salesProductRepository.GetAllocationProvidersAsync(product.Id);
var channelId = providers
.Where(x => x.ServiceChannelId is > 0)
.OrderBy(x => x.SerialNumber)
.Select(x => x.ServiceChannelId!.Value)
.FirstOrDefault();
if (channelId <= 0)
{
return null;
}
var channel = await _serviceChannelRepository.FirstOrDefaultAsync(x => x.Id == channelId);
if (channel?.GroupId is not > 0)
{
return null;
}
var group = await _serviceChannelGroupRepository.FirstOrDefaultAsync(x => x.Id == channel.GroupId.Value);
if (group == null || group.AppId <= 0)
{
return null;
}
var app = await _itfxMetadataClient.GetAppAsync(group.AppId);
return app?.AppKey?.Trim();
}
private async Task<(decimal VolumetricWeight, decimal ChargeableWeight)> CalculateChargeableAsync(
SalesProduct product,
OrderWeight weight)

View File

@ -190,10 +190,7 @@ public class SystemOtherConfigService : ApplicationService
}
else if (string.Equals(configCode, SystemOtherConfigCodes.PickupOpenApi, StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(input.AppKey))
{
throw new BusinessException("揽收开放接口 AppKey 不能为空");
}
// 鉴权已改由 itfx 上游完成;本配置不再强制 AppKey
}
else if (string.Equals(configCode, SystemOtherConfigCodes.WeChatMp, StringComparison.OrdinalIgnoreCase))
{

View File

@ -56,7 +56,7 @@ public static class SystemOtherConfigCodes
public const string ApiCenter = "api_center";
/// <summary>
/// 国内揽收服务商推送件重尺/费用:仅校验 AppKey(暂不验签)。
/// 国内揽收服务商推送件重尺/费用(历史配置;鉴权已改由 itfx 上游应用完成,OMS 不再校验 AppKey)。
/// </summary>
public const string PickupOpenApi = "pickup_open_api";
}