20261008-ide1.0发布 #1
2
db.sql
2
db.sql
@ -214,7 +214,7 @@ CREATE TABLE `t_order` (
|
|||||||
CREATE TABLE `t_order_file` (
|
CREATE TABLE `t_order_file` (
|
||||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增',
|
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增',
|
||||||
`OrderId` INT NOT NULL COMMENT '订单Id',
|
`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 '原始文件名',
|
`FileName` VARCHAR(255) NULL COMMENT '原始文件名',
|
||||||
`FileUrl` VARCHAR(512) NOT NULL COMMENT '文件地址(相对路径或外链)',
|
`FileUrl` VARCHAR(512) NOT NULL COMMENT '文件地址(相对路径或外链)',
|
||||||
`ContentType` VARCHAR(128) NULL COMMENT '文件MIME类型',
|
`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;
|
namespace Oms.Application.Integrations.SmallBagTrack;
|
||||||
|
|
||||||
public interface ISmallBagTrackOrderClient
|
public interface ISmallBagTrackOrderClient
|
||||||
@ -45,4 +47,11 @@ public interface ISmallBagTrackOrderClient
|
|||||||
int linkOrderType,
|
int linkOrderType,
|
||||||
string? authorization = null,
|
string? authorization = null,
|
||||||
CancellationToken cancellationToken = default);
|
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.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Fake.Application;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Oms.Application.Dtos.OpenApi;
|
||||||
|
|
||||||
namespace Oms.Application.Integrations.SmallBagTrack;
|
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>(
|
private async Task<SignedTrackHttpResponse> SendSignedPostAsync<TBody>(
|
||||||
SmallBagTrackOptions settings,
|
SmallBagTrackOptions settings,
|
||||||
string appKey,
|
string appKey,
|
||||||
|
|||||||
@ -181,13 +181,14 @@ public class CustomerOpenApiCredentialService : ApplicationService
|
|||||||
{
|
{
|
||||||
if (!OmsSessionHelper.IsCustomerLogin(CurrentUser))
|
if (!OmsSessionHelper.IsCustomerLogin(CurrentUser))
|
||||||
{
|
{
|
||||||
throw new BusinessException("仅客户门户可调用此接口");
|
throw new BusinessException(
|
||||||
|
"当前是管理后台登录,不能调用客户门户接口。请到「客户」列表打开对应客户的开放接口凭证;客户请从客户门户登录后操作");
|
||||||
}
|
}
|
||||||
|
|
||||||
var mapId = OmsSessionHelper.TryGetCustomerMapId(CurrentUser);
|
var mapId = OmsSessionHelper.TryGetCustomerMapId(CurrentUser);
|
||||||
if (!mapId.HasValue || mapId.Value <= 0)
|
if (!mapId.HasValue || mapId.Value <= 0)
|
||||||
{
|
{
|
||||||
throw new BusinessException("客户未登录");
|
throw new BusinessException("当前登录不是客户账号,无法读取开放接口凭证");
|
||||||
}
|
}
|
||||||
|
|
||||||
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.UserId == mapId.Value);
|
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.UserId == mapId.Value);
|
||||||
|
|||||||
@ -6,8 +6,10 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Oms.Application.Dtos.OpenApi;
|
using Oms.Application.Dtos.OpenApi;
|
||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
|
using Oms.Application.Integrations.SmallBagTrack;
|
||||||
using Oms.Application.Services.Orders;
|
using Oms.Application.Services.Orders;
|
||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
|
||||||
namespace Oms.Application.Services.OpenApi;
|
namespace Oms.Application.Services.OpenApi;
|
||||||
|
|
||||||
@ -33,6 +35,15 @@ public class OpenApiService : ApplicationService
|
|||||||
private OrderService OrderService =>
|
private OrderService OrderService =>
|
||||||
LazyServiceProvider.GetRequiredService<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 =>
|
private IHttpContextAccessor HttpContextAccessor =>
|
||||||
LazyServiceProvider.GetRequiredService<IHttpContextAccessor>();
|
LazyServiceProvider.GetRequiredService<IHttpContextAccessor>();
|
||||||
|
|
||||||
@ -84,7 +95,8 @@ public class OpenApiService : ApplicationService
|
|||||||
throw new BusinessException("请求体不能为空");
|
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);
|
return OpenApiEnvelope<OrderParcelCreateOutput>.Ok(data);
|
||||||
}
|
}
|
||||||
catch (BusinessException ex)
|
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()
|
private async Task<string> ReadRawBodyAsync()
|
||||||
{
|
{
|
||||||
var http = HttpContextAccessor.HttpContext
|
var http = HttpContextAccessor.HttpContext
|
||||||
|
|||||||
@ -66,6 +66,25 @@ public sealed class OrderFileManager(
|
|||||||
return result;
|
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(
|
public async Task<OrderFileGroupedOutput> GetGroupedAsync(
|
||||||
int orderId,
|
int orderId,
|
||||||
bool customerView,
|
bool customerView,
|
||||||
@ -302,7 +321,8 @@ public sealed class OrderFileManager(
|
|||||||
or OrderFileTypeCodes.DownstreamLabel
|
or OrderFileTypeCodes.DownstreamLabel
|
||||||
or OrderFileTypeCodes.LastMileLabel;
|
or OrderFileTypeCodes.LastMileLabel;
|
||||||
var isInfraredOrProof = fileType is OrderFileTypeCodes.InfraredImage
|
var isInfraredOrProof = fileType is OrderFileTypeCodes.InfraredImage
|
||||||
or OrderFileTypeCodes.TradeProof;
|
or OrderFileTypeCodes.TradeProof
|
||||||
|
or OrderFileTypeCodes.Pod;
|
||||||
|
|
||||||
if (isLabelType)
|
if (isLabelType)
|
||||||
{
|
{
|
||||||
@ -325,7 +345,7 @@ public sealed class OrderFileManager(
|
|||||||
{
|
{
|
||||||
if (!ImageExt.Contains(ext) && !string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase))
|
if (!ImageExt.Contains(ext) && !string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
throw new BusinessException("交易证明/红外图片仅支持图片或 pdf");
|
throw new BusinessException("交易证明/红外图片/POD 仅支持图片或 pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.Length > MaxPdfBytes)
|
if (file.Length > MaxPdfBytes)
|
||||||
|
|||||||
@ -531,6 +531,46 @@ public partial class OrderService : ApplicationService
|
|||||||
return output;
|
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(
|
private async Task TryAttachPickupAfterParcelCreateAsync(
|
||||||
OrderParcelCreateInput input,
|
OrderParcelCreateInput input,
|
||||||
OrderParcelCreateOutput output)
|
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 DownstreamLabel = "downstreamLabel";
|
||||||
public const string LastMileLabel = "lastMileLabel";
|
public const string LastMileLabel = "lastMileLabel";
|
||||||
public const string InfraredImage = "infraredImage";
|
public const string InfraredImage = "infraredImage";
|
||||||
|
public const string Pod = "pod";
|
||||||
|
|
||||||
public const int MaxFilesPerType = 20;
|
public const int MaxFilesPerType = 20;
|
||||||
|
|
||||||
@ -19,7 +20,8 @@ public static class OrderFileTypeCodes
|
|||||||
SystemLabel,
|
SystemLabel,
|
||||||
DownstreamLabel,
|
DownstreamLabel,
|
||||||
LastMileLabel,
|
LastMileLabel,
|
||||||
InfraredImage
|
InfraredImage,
|
||||||
|
Pod
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -31,6 +33,19 @@ public static class OrderFileTypeCodes
|
|||||||
public static bool IsValid(string? fileType) =>
|
public static bool IsValid(string? fileType) =>
|
||||||
!string.IsNullOrWhiteSpace(fileType) && All.Contains(fileType.Trim());
|
!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();
|
public static string Normalize(string fileType) => fileType.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ public class OrderFile : OmsAuditedAggregateRoot
|
|||||||
public int OrderId { get; set; }
|
public int OrderId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 附件类型:tradeProof / systemLabel / downstreamLabel / lastMileLabel / infraredImage。
|
/// 附件类型:tradeProof / systemLabel / downstreamLabel / lastMileLabel / infraredImage / pod。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SugarColumn(Length = 32)]
|
[SugarColumn(Length = 32)]
|
||||||
public string FileType { get; set; } = string.Empty;
|
public string FileType { get; set; } = string.Empty;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user