feat: OpenAPI 增加轨迹/文件查询,创单按 clientNo 幂等
客户 HMAC 通过后再查本客户订单;禁止下游面单,附件增加 POD。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b748aa4000
commit
f063c99d1b
2
db.sql
2
db.sql
@ -214,7 +214,7 @@ CREATE TABLE `t_order` (
|
||||
CREATE TABLE `t_order_file` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增',
|
||||
`OrderId` INT NOT NULL COMMENT '订单Id',
|
||||
`FileType` VARCHAR(32) NOT NULL COMMENT '附件类型:tradeProof交易证明/systemLabel系统面单/downstreamLabel下游面单/lastMileLabel末端面单/infraredImage红外图片',
|
||||
`FileType` VARCHAR(32) NOT NULL COMMENT '附件类型:tradeProof交易证明/systemLabel系统面单/downstreamLabel下游面单/lastMileLabel末端面单/infraredImage红外图片/pod签收证明',
|
||||
`FileName` VARCHAR(255) NULL COMMENT '原始文件名',
|
||||
`FileUrl` VARCHAR(512) NOT NULL COMMENT '文件地址(相对路径或外链)',
|
||||
`ContentType` VARCHAR(128) NULL COMMENT '文件MIME类型',
|
||||
|
||||
92
src/Oms.Application/Dtos/OpenApi/OpenApiQueryDtos.cs
Normal file
92
src/Oms.Application/Dtos/OpenApi/OpenApiQueryDtos.cs
Normal file
@ -0,0 +1,92 @@
|
||||
namespace Oms.Application.Dtos.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// 按客户单号 / 服务单号 / 跟踪号定位订单(至少填一项)。
|
||||
/// </summary>
|
||||
public class OpenApiOrderLookupInput
|
||||
{
|
||||
public string? ClientNo { get; set; }
|
||||
|
||||
public string? ServiceNo { get; set; }
|
||||
|
||||
public string? TrackNo { get; set; }
|
||||
}
|
||||
|
||||
public class OpenApiTrackQueryInput : OpenApiOrderLookupInput
|
||||
{
|
||||
}
|
||||
|
||||
public class OpenApiFilesGetInput : OpenApiOrderLookupInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 附件类型:tradeProof / systemLabel / lastMileLabel / infraredImage / pod。不含 downstreamLabel。
|
||||
/// </summary>
|
||||
public string? FileType { get; set; }
|
||||
}
|
||||
|
||||
public class OpenApiTrackQueryOutput
|
||||
{
|
||||
public int OrderId { get; set; }
|
||||
|
||||
public string? ClientNo { get; set; }
|
||||
|
||||
public string? ServiceNo { get; set; }
|
||||
|
||||
public string? TrackNo { get; set; }
|
||||
|
||||
public List<OpenApiTrackEventDto> Events { get; set; } = [];
|
||||
}
|
||||
|
||||
public class OpenApiTrackEventDto
|
||||
{
|
||||
public string? TrackCode { get; set; }
|
||||
|
||||
public string? Status { get; set; }
|
||||
|
||||
public string? Content { get; set; }
|
||||
|
||||
public string? EnContent { get; set; }
|
||||
|
||||
public string? Location { get; set; }
|
||||
|
||||
public DateTime? Time { get; set; }
|
||||
|
||||
public List<OpenApiTrackFileDto> Files { get; set; } = [];
|
||||
}
|
||||
|
||||
public class OpenApiTrackFileDto
|
||||
{
|
||||
public string? FileName { get; set; }
|
||||
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
|
||||
public class OpenApiFilesGetOutput
|
||||
{
|
||||
public int OrderId { get; set; }
|
||||
|
||||
public string? ClientNo { get; set; }
|
||||
|
||||
public string? ServiceNo { get; set; }
|
||||
|
||||
public string FileType { get; set; } = string.Empty;
|
||||
|
||||
public List<OpenApiFileItemDto> Files { get; set; } = [];
|
||||
}
|
||||
|
||||
public class OpenApiFileItemDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string? FileName { get; set; }
|
||||
|
||||
public string FileUrl { get; set; } = string.Empty;
|
||||
|
||||
public string? ContentType { get; set; }
|
||||
|
||||
public long? FileSize { get; set; }
|
||||
|
||||
public string? Source { get; set; }
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
using Oms.Application.Dtos.OpenApi;
|
||||
|
||||
namespace Oms.Application.Integrations.SmallBagTrack;
|
||||
|
||||
public interface ISmallBagTrackOrderClient
|
||||
@ -45,4 +47,11 @@ public interface ISmallBagTrackOrderClient
|
||||
int linkOrderType,
|
||||
string? authorization = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询小包轨迹(POST ProviderService/Track/Query)。
|
||||
/// </summary>
|
||||
Task<List<OpenApiTrackEventDto>> QuerySmallBagTracksAsync(
|
||||
string orderNo,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Dtos.OpenApi;
|
||||
|
||||
namespace Oms.Application.Integrations.SmallBagTrack;
|
||||
|
||||
@ -424,6 +426,178 @@ internal sealed class SmallBagTrackOrderClient(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<OpenApiTrackEventDto>> QuerySmallBagTracksAsync(
|
||||
string orderNo,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(orderNo))
|
||||
{
|
||||
throw new BusinessException("服务单号为空,无法查询轨迹");
|
||||
}
|
||||
|
||||
var settings = await configProvider.GetAsync(cancellationToken);
|
||||
if (!settings.Enabled)
|
||||
{
|
||||
throw new BusinessException("轨迹服务未启用");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.BaseUrl))
|
||||
{
|
||||
throw new BusinessException("轨迹服务未配置");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.AppKey) || string.IsNullOrWhiteSpace(settings.AppSecret))
|
||||
{
|
||||
throw new BusinessException("轨迹服务未配置");
|
||||
}
|
||||
|
||||
var appKey = settings.AppKey.Trim();
|
||||
var url = $"{settings.BaseUrl.Trim().TrimEnd('/')}/ProviderService/Track/Query";
|
||||
var body = new
|
||||
{
|
||||
appKey,
|
||||
orderType = SmallBagTrackLifecycleEvents.OrderType,
|
||||
orderNos = new[] { orderNo.Trim() }
|
||||
};
|
||||
|
||||
var response = await SendSignedPostAsync(
|
||||
settings,
|
||||
appKey,
|
||||
url,
|
||||
body,
|
||||
authorization: null,
|
||||
cancellationToken);
|
||||
|
||||
if (!response.Response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new BusinessException("轨迹查询失败");
|
||||
}
|
||||
|
||||
return ParseTrackEvents(response.ResponseBody);
|
||||
}
|
||||
|
||||
private static List<OpenApiTrackEventDto> ParseTrackEvents(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
var arr = root;
|
||||
if (root.ValueKind == JsonValueKind.Object
|
||||
&& (root.TryGetProperty("data", out var data) || root.TryGetProperty("Data", out data)))
|
||||
{
|
||||
arr = data;
|
||||
}
|
||||
|
||||
if (arr.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var events = new List<OpenApiTrackEventDto>();
|
||||
foreach (var orderEl in arr.EnumerateArray())
|
||||
{
|
||||
if (orderEl.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!orderEl.TryGetProperty("trackDetails", out var details)
|
||||
&& !orderEl.TryGetProperty("TrackDetails", out details))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (details.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var el in details.EnumerateArray())
|
||||
{
|
||||
var content = ReadJsonString(el, "content", "Content");
|
||||
if (string.IsNullOrWhiteSpace(content) || content == "--")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
events.Add(new OpenApiTrackEventDto
|
||||
{
|
||||
TrackCode = ReadJsonString(el, "code", "Code"),
|
||||
Status = ReadJsonString(el, "status", "Status"),
|
||||
Content = content,
|
||||
EnContent = ReadJsonString(el, "enContent", "EnContent"),
|
||||
Location = ReadJsonString(el, "location", "Location"),
|
||||
Time = ReadJsonTime(el),
|
||||
Files = ReadJsonFiles(el)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
private static List<OpenApiTrackFileDto> ReadJsonFiles(JsonElement el)
|
||||
{
|
||||
if (!el.TryGetProperty("files", out var files) && !el.TryGetProperty("Files", out files))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (files.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var list = new List<OpenApiTrackFileDto>();
|
||||
foreach (var f in files.EnumerateArray())
|
||||
{
|
||||
list.Add(new OpenApiTrackFileDto
|
||||
{
|
||||
FileName = ReadJsonString(f, "fileName", "FileName"),
|
||||
Url = ReadJsonString(f, "downloadUrl", "DownloadUrl", "url", "Url")
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static DateTime? ReadJsonTime(JsonElement el)
|
||||
{
|
||||
if (!el.TryGetProperty("time", out var time) && !el.TryGetProperty("Time", out time))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (time.ValueKind == JsonValueKind.String && time.TryGetDateTime(out var dt))
|
||||
{
|
||||
return dt;
|
||||
}
|
||||
|
||||
if (time.ValueKind == JsonValueKind.Number && time.TryGetInt64(out var unix))
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeSeconds(unix > 10_000_000_000 ? unix / 1000 : unix).UtcDateTime;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? ReadJsonString(JsonElement el, params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (el.TryGetProperty(name, out var p) && p.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return p.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<SignedTrackHttpResponse> SendSignedPostAsync<TBody>(
|
||||
SmallBagTrackOptions settings,
|
||||
string appKey,
|
||||
|
||||
@ -181,13 +181,14 @@ public class CustomerOpenApiCredentialService : ApplicationService
|
||||
{
|
||||
if (!OmsSessionHelper.IsCustomerLogin(CurrentUser))
|
||||
{
|
||||
throw new BusinessException("仅客户门户可调用此接口");
|
||||
throw new BusinessException(
|
||||
"当前是管理后台登录,不能调用客户门户接口。请到「客户」列表打开对应客户的开放接口凭证;客户请从客户门户登录后操作");
|
||||
}
|
||||
|
||||
var mapId = OmsSessionHelper.TryGetCustomerMapId(CurrentUser);
|
||||
if (!mapId.HasValue || mapId.Value <= 0)
|
||||
{
|
||||
throw new BusinessException("客户未登录");
|
||||
throw new BusinessException("当前登录不是客户账号,无法读取开放接口凭证");
|
||||
}
|
||||
|
||||
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.UserId == mapId.Value);
|
||||
|
||||
@ -6,8 +6,10 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.OpenApi;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Integrations.SmallBagTrack;
|
||||
using Oms.Application.Services.Orders;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
|
||||
namespace Oms.Application.Services.OpenApi;
|
||||
|
||||
@ -33,6 +35,15 @@ public class OpenApiService : ApplicationService
|
||||
private OrderService OrderService =>
|
||||
LazyServiceProvider.GetRequiredService<OrderService>();
|
||||
|
||||
private OrderFileManager OrderFileManager =>
|
||||
LazyServiceProvider.GetRequiredService<OrderFileManager>();
|
||||
|
||||
private OpenApiCustomerOrderResolver OrderResolver =>
|
||||
LazyServiceProvider.GetRequiredService<OpenApiCustomerOrderResolver>();
|
||||
|
||||
private ISmallBagTrackOrderClient TrackClient =>
|
||||
LazyServiceProvider.GetRequiredService<ISmallBagTrackOrderClient>();
|
||||
|
||||
private IHttpContextAccessor HttpContextAccessor =>
|
||||
LazyServiceProvider.GetRequiredService<IHttpContextAccessor>();
|
||||
|
||||
@ -84,7 +95,8 @@ public class OpenApiService : ApplicationService
|
||||
throw new BusinessException("请求体不能为空");
|
||||
}
|
||||
|
||||
var data = await OrderService.CreateParcelAsync(input.ToOrderParcelCreateInput(customer.Id));
|
||||
var data = await OrderService.CreateParcelForCustomerOpenApiAsync(
|
||||
input.ToOrderParcelCreateInput(customer.Id));
|
||||
return OpenApiEnvelope<OrderParcelCreateOutput>.Ok(data);
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
@ -103,6 +115,131 @@ public class OpenApiService : ApplicationService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询轨迹。Header 同创建包裹。按 clientNo / serviceNo / trackNo 定位本客户订单。
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("/openapi/track/query")]
|
||||
public async Task<OpenApiEnvelope<OpenApiTrackQueryOutput>> QueryTrackAsync(
|
||||
[FromHeader(Name = CustomerOpenApiAuthHelper.AppKeyHeaderName)] string? appKey,
|
||||
[FromHeader(Name = CustomerOpenApiAuthHelper.TimestampHeaderName)] string? timestamp,
|
||||
[FromHeader(Name = CustomerOpenApiAuthHelper.SignHeaderName)] string? sign)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rawBody = await ReadRawBodyAsync();
|
||||
var (customer, _) = await AuthHelper.ResolveSignedAsync(appKey, timestamp, sign, rawBody);
|
||||
var input = DeserializeBody<OpenApiTrackQueryInput>(rawBody);
|
||||
var order = await OrderResolver.ResolveAsync(customer.Id, input);
|
||||
if (string.IsNullOrWhiteSpace(order.ServiceNo))
|
||||
{
|
||||
throw new BusinessException("订单无服务单号,无法查询轨迹");
|
||||
}
|
||||
|
||||
var events = await TrackClient.QuerySmallBagTracksAsync(order.ServiceNo);
|
||||
return OpenApiEnvelope<OpenApiTrackQueryOutput>.Ok(new OpenApiTrackQueryOutput
|
||||
{
|
||||
OrderId = order.Id,
|
||||
ClientNo = order.ClientNo,
|
||||
ServiceNo = order.ServiceNo,
|
||||
TrackNo = order.TrackNo,
|
||||
Events = events
|
||||
});
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return OpenApiEnvelope<OpenApiTrackQueryOutput>.Fail(ex.Message);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return OpenApiEnvelope<OpenApiTrackQueryOutput>.Fail("请求体格式错误");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return OpenApiEnvelope<OpenApiTrackQueryOutput>.Fail(
|
||||
string.IsNullOrWhiteSpace(ex.Message) ? "系统异常" : ex.Message,
|
||||
"500");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按附件类型获取文件。Header 同创建包裹。
|
||||
/// fileType:tradeProof / systemLabel / lastMileLabel / infraredImage / pod。不含 downstreamLabel。
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("/openapi/files/get")]
|
||||
public async Task<OpenApiEnvelope<OpenApiFilesGetOutput>> GetFilesAsync(
|
||||
[FromHeader(Name = CustomerOpenApiAuthHelper.AppKeyHeaderName)] string? appKey,
|
||||
[FromHeader(Name = CustomerOpenApiAuthHelper.TimestampHeaderName)] string? timestamp,
|
||||
[FromHeader(Name = CustomerOpenApiAuthHelper.SignHeaderName)] string? sign)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rawBody = await ReadRawBodyAsync();
|
||||
var (customer, _) = await AuthHelper.ResolveSignedAsync(appKey, timestamp, sign, rawBody);
|
||||
var input = DeserializeBody<OpenApiFilesGetInput>(rawBody);
|
||||
if (string.IsNullOrWhiteSpace(input.FileType))
|
||||
{
|
||||
throw new BusinessException("请传入 fileType");
|
||||
}
|
||||
|
||||
if (!OrderFileTypeCodes.IsCustomerOpenApiAllowed(input.FileType))
|
||||
{
|
||||
throw new BusinessException("不支持的附件类型");
|
||||
}
|
||||
|
||||
var order = await OrderResolver.ResolveAsync(customer.Id, input);
|
||||
var files = await OrderFileManager.GetByTypeAsync(order.Id, input.FileType);
|
||||
return OpenApiEnvelope<OpenApiFilesGetOutput>.Ok(new OpenApiFilesGetOutput
|
||||
{
|
||||
OrderId = order.Id,
|
||||
ClientNo = order.ClientNo,
|
||||
ServiceNo = order.ServiceNo,
|
||||
FileType = OrderFileTypeCodes.Normalize(input.FileType),
|
||||
Files = files.Select(x => new OpenApiFileItemDto
|
||||
{
|
||||
Id = x.Id,
|
||||
FileName = x.FileName,
|
||||
FileUrl = x.FileUrl,
|
||||
ContentType = x.ContentType,
|
||||
FileSize = x.FileSize,
|
||||
Source = x.Source,
|
||||
CreateTime = x.CreateTime
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return OpenApiEnvelope<OpenApiFilesGetOutput>.Fail(ex.Message);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return OpenApiEnvelope<OpenApiFilesGetOutput>.Fail("请求体格式错误");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return OpenApiEnvelope<OpenApiFilesGetOutput>.Fail(
|
||||
string.IsNullOrWhiteSpace(ex.Message) ? "系统异常" : ex.Message,
|
||||
"500");
|
||||
}
|
||||
}
|
||||
|
||||
private T DeserializeBody<T>(string rawBody)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawBody))
|
||||
{
|
||||
throw new BusinessException("请求体不能为空");
|
||||
}
|
||||
|
||||
var input = JsonSerializer.Deserialize<T>(rawBody, BodyJsonOptions);
|
||||
if (input == null)
|
||||
{
|
||||
throw new BusinessException("请求体不能为空");
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
private async Task<string> ReadRawBodyAsync()
|
||||
{
|
||||
var http = HttpContextAccessor.HttpContext
|
||||
|
||||
@ -66,6 +66,25 @@ public sealed class OrderFileManager(
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<OrderFileItemOutput>> GetByTypeAsync(
|
||||
int orderId,
|
||||
string fileType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!OrderFileTypeCodes.IsValid(fileType))
|
||||
{
|
||||
throw new BusinessException("不支持的附件类型");
|
||||
}
|
||||
|
||||
var type = OrderFileTypeCodes.Normalize(fileType);
|
||||
var list = await orderFileRepository.GetByOrderAndTypeListAsync(orderId, type, cancellationToken);
|
||||
return list
|
||||
.OrderBy(x => x.Sort)
|
||||
.ThenBy(x => x.Id)
|
||||
.Select(MapItem)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<OrderFileGroupedOutput> GetGroupedAsync(
|
||||
int orderId,
|
||||
bool customerView,
|
||||
@ -302,7 +321,8 @@ public sealed class OrderFileManager(
|
||||
or OrderFileTypeCodes.DownstreamLabel
|
||||
or OrderFileTypeCodes.LastMileLabel;
|
||||
var isInfraredOrProof = fileType is OrderFileTypeCodes.InfraredImage
|
||||
or OrderFileTypeCodes.TradeProof;
|
||||
or OrderFileTypeCodes.TradeProof
|
||||
or OrderFileTypeCodes.Pod;
|
||||
|
||||
if (isLabelType)
|
||||
{
|
||||
@ -325,7 +345,7 @@ public sealed class OrderFileManager(
|
||||
{
|
||||
if (!ImageExt.Contains(ext) && !string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new BusinessException("交易证明/红外图片仅支持图片或 pdf");
|
||||
throw new BusinessException("交易证明/红外图片/POD 仅支持图片或 pdf");
|
||||
}
|
||||
|
||||
if (file.Length > MaxPdfBytes)
|
||||
|
||||
@ -531,6 +531,46 @@ public partial class OrderService : ApplicationService
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 客户 OpenAPI 创单:同一客户相同 clientNo 视为幂等,返回已存在订单,不新建。
|
||||
/// </summary>
|
||||
public async Task<OrderParcelCreateOutput> CreateParcelForCustomerOpenApiAsync(OrderParcelCreateInput input)
|
||||
{
|
||||
NormalizeParcelCreateInput(input);
|
||||
if (!string.IsNullOrWhiteSpace(input.ClientNo) && input.CustomerId is > 0)
|
||||
{
|
||||
var existing = await OrderRepository.GetByCustomerAndClientNoAsync(
|
||||
input.CustomerId.Value,
|
||||
input.ClientNo);
|
||||
if (existing != null)
|
||||
{
|
||||
return await BuildParcelCreateOutputAsync(existing);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await CreateParcelAsync(input);
|
||||
}
|
||||
catch (BusinessException ex) when (ex.Message == "客户单号已存在")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.ClientNo) || input.CustomerId is not > 0)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
var raced = await OrderRepository.GetByCustomerAndClientNoAsync(
|
||||
input.CustomerId.Value,
|
||||
input.ClientNo);
|
||||
if (raced == null)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
return await BuildParcelCreateOutputAsync(raced);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TryAttachPickupAfterParcelCreateAsync(
|
||||
OrderParcelCreateInput input,
|
||||
OrderParcelCreateOutput output)
|
||||
|
||||
54
src/Oms.Application/Shared/OpenApiCustomerOrderResolver.cs
Normal file
54
src/Oms.Application/Shared/OpenApiCustomerOrderResolver.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using Fake.Application;
|
||||
using Fake.DependencyInjection;
|
||||
using Oms.Application.Dtos.OpenApi;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAPI 按客户凭证解析自有订单。
|
||||
/// </summary>
|
||||
public class OpenApiCustomerOrderResolver : IScopedDependency
|
||||
{
|
||||
private readonly IOrderRepository _orderRepository;
|
||||
|
||||
public OpenApiCustomerOrderResolver(IOrderRepository orderRepository)
|
||||
{
|
||||
_orderRepository = orderRepository;
|
||||
}
|
||||
|
||||
public async Task<Order> ResolveAsync(int customerId, OpenApiOrderLookupInput input)
|
||||
{
|
||||
var clientNo = input.ClientNo?.Trim();
|
||||
var serviceNo = input.ServiceNo?.Trim();
|
||||
var trackNo = input.TrackNo?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(clientNo)
|
||||
&& string.IsNullOrWhiteSpace(serviceNo)
|
||||
&& string.IsNullOrWhiteSpace(trackNo))
|
||||
{
|
||||
throw new BusinessException("请传入 clientNo、serviceNo 或 trackNo");
|
||||
}
|
||||
|
||||
Order? order = null;
|
||||
if (!string.IsNullOrWhiteSpace(clientNo))
|
||||
{
|
||||
order = await _orderRepository.GetByCustomerAndClientNoAsync(customerId, clientNo);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(serviceNo))
|
||||
{
|
||||
order = await _orderRepository.GetByServiceNoAsync(serviceNo);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(trackNo))
|
||||
{
|
||||
order = await _orderRepository.GetByTrackNoAsync(trackNo);
|
||||
}
|
||||
|
||||
if (order == null || order.CustomerId != customerId)
|
||||
{
|
||||
throw new BusinessException("订单不存在");
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ public static class OrderFileTypeCodes
|
||||
public const string DownstreamLabel = "downstreamLabel";
|
||||
public const string LastMileLabel = "lastMileLabel";
|
||||
public const string InfraredImage = "infraredImage";
|
||||
public const string Pod = "pod";
|
||||
|
||||
public const int MaxFilesPerType = 20;
|
||||
|
||||
@ -19,7 +20,8 @@ public static class OrderFileTypeCodes
|
||||
SystemLabel,
|
||||
DownstreamLabel,
|
||||
LastMileLabel,
|
||||
InfraredImage
|
||||
InfraredImage,
|
||||
Pod
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@ -31,6 +33,19 @@ public static class OrderFileTypeCodes
|
||||
public static bool IsValid(string? fileType) =>
|
||||
!string.IsNullOrWhiteSpace(fileType) && All.Contains(fileType.Trim());
|
||||
|
||||
/// <summary>
|
||||
/// 客户 OpenAPI 可查询的附件类型。不含下游面单。
|
||||
/// </summary>
|
||||
public static bool IsCustomerOpenApiAllowed(string? fileType)
|
||||
{
|
||||
if (!IsValid(fileType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !string.Equals(fileType.Trim(), DownstreamLabel, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string Normalize(string fileType) => fileType.Trim();
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ public class OrderFile : OmsAuditedAggregateRoot
|
||||
public int OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 附件类型:tradeProof / systemLabel / downstreamLabel / lastMileLabel / infraredImage。
|
||||
/// 附件类型:tradeProof / systemLabel / downstreamLabel / lastMileLabel / infraredImage / pod。
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 32)]
|
||||
public string FileType { get; set; } = string.Empty;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user