20261008-ide1.0发布 #1

Merged
zzh merged 42 commits from test into master 2026-10-08 09:18:00 +08:00
11 changed files with 550 additions and 8 deletions
Showing only changes of commit f063c99d1b - Show all commits

2
db.sql
View File

@ -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类型',

View 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; }
}

View File

@ -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);
} }

View File

@ -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,

View File

@ -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);

View File

@ -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

View File

@ -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)

View File

@ -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)

View 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;
}
}

View File

@ -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();
} }

View File

@ -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;