This commit is contained in:
xiaolipro 2025-11-29 17:23:19 +08:00
parent 7b3f083b9b
commit 1966976944
12 changed files with 780 additions and 133 deletions

View File

@ -15,7 +15,7 @@ using TrackCenter.Domain.Extensions;
using TrackCenter.Repository;
using TrackCenter.Repository.Otel;
using TrackCenter.Repository.Otel.Options;
using Microsoft.Extensions.DependencyInjection;
using TrackCenter.Application.Filters;
namespace TrackCenter.Api;
@ -37,6 +37,9 @@ public class TrackCenterApiModule : FakeModule
options.ScanApplicationServices<TrackCenterApplicationModule>();
});
// 配置签名验证选项
services.Configure<SignatureValidationOptions>(configuration.GetSection("SignatureValidation"));
// 全局授权配置:所有接口默认需要授权
services.AddMvc(options =>
{
@ -44,6 +47,8 @@ public class TrackCenterApiModule : FakeModule
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
// 添加签名验证过滤器
options.Filters.Add<SignatureValidationFilter>();
}).AddNewtonsoftJson();
services.AddFakeExceptionFilter()

View File

@ -187,5 +187,9 @@
"ExpiredTime": 50000, // 过期时间,long 类型,单位分钟,默认20分钟
"ClockSkew": 10, // 过期时间容错值,long 类型,单位秒,默认5秒
"Algorithm": "HS256" // 加密算法,string 类型,默认 HS256
},
"SignatureValidation": {
"Enabled": true, // 是否启用签名验证
"MaxTimestampOffset": 300 // 时间戳允许的最大偏差(秒),默认300秒(5分钟)
}
}

View File

@ -0,0 +1,189 @@
using System.ComponentModel.DataAnnotations;
namespace TrackCenter.Application.Dtos.ProvideServices.V2;
public class UpsertSmallBagInput
{
/// <summary>
/// 客户名称,或者code,必填
/// </summary>
[Required]
public string Client { get; set; }
/// <summary>
/// 客户单号,必填
/// </summary>
[Required]
public string OrderNo { get; set; }
/// <summary>
/// 尾程单号,必填
/// </summary>
[Required]
public string TrackNo { get; set; }
/// <summary>
/// 其他单号
/// </summary>
public string OtherNo { get; set; }
/// <summary>
/// 渠道名称,或者code
/// </summary>
public string Channel { get; set; }
/// <summary>
/// 产品名称,或者code
/// </summary>
public string Product { get; set; }
/// <summary>
/// 服务商code
/// </summary>
public string ApiCode { get; set; }
/// <summary>
/// 收件人信息
/// </summary>
public Consignee Consignee { get; set; }
}
public class Consignee
{
/// <summary>
/// 收件人国家,必填
/// </summary>
[Required]
public string CountryCode { get; set; }
/// <summary>
/// 收件人州省
/// </summary>
public string Province { get; set; }
/// <summary>
/// 收件人邮编
/// </summary>
public string Postcode { get; set; }
/// <summary>
/// 收件人城市
/// </summary>
public string City { get; set; }
/// <summary>
/// 收件人邮箱
/// </summary>
public string Email { get; set; }
}
public class UpsertBigBagInput
{
/// <summary>
/// 客户名称,或者code,必填
/// </summary>
[Required]
public string Client { get; set; }
/// <summary>
/// 大包号,必填
/// </summary>
[Required]
public string BagNo { get; set; }
/// <summary>
/// 其他单号
/// </summary>
public string OtherNo { get; set; }
/// <summary>
/// 渠道名称,或者code
/// </summary>
public string Channel { get; set; }
/// <summary>
/// 产品名称,或者code
/// </summary>
public string Product { get; set; }
/// <summary>
/// 产品名称,或者code
/// </summary>
public string Country { get; set; }
}
public class UpsertMasterOrderInput
{
/// <summary>
/// 服务商code
/// </summary>
public string ApiCode { get; set; }
/// <summary>
/// 客户名称,或者code,必填
/// </summary>
[Required]
public string Client { get; set; }
/// <summary>
/// 主单号,必填
/// </summary>
[Required]
public string MasterNo { get; set; }
/// <summary>
/// 是否中转,必填
/// </summary>
[Required]
public bool IsTransfer { get; set; }
/// <summary>
/// 目的国家二字码,必填
/// </summary>
[Required]
public string ArrivalCountry { get; set; }
/// <summary>
/// 起运国家二字码,必填
/// </summary>
[Required]
public string LeaveCountry { get; set; }
/// <summary>
/// 航班信息(传当前订单的所有航班信息)
/// </summary>
public List<FlightInfo> FlightInfos { get; set; }
}
/// <summary>
/// 航班信息
/// </summary>
public class FlightInfo
{
/// <summary>
/// 航班号
/// </summary>
public string FlightNo { get; set; }
/// <summary>
/// 起运港口三字码
/// </summary>
public string LeavePort { get; set; }
/// <summary>
/// 目的港口三字码
/// </summary>
public string ArrivalPort { get; set; }
/// <summary>
/// 预计到达时间
/// </summary>
public string Eta { get; set; }
/// <summary>
/// 预计起飞时间
/// </summary>
public string Etd { get; set; }
}

View File

@ -0,0 +1,145 @@
# 签名验证Filter使用说明
## 概述
签名验证Filter用于验证API请求的签名,确保请求的合法性和安全性。
## 配置
在 `appsettings.json` 中配置签名验证选项:
```json
"SignatureValidation": {
"Enabled": true, // 是否启用签名验证
"MaxTimestampOffset": 300, // 时间戳允许的最大偏差(秒),默认300秒(5分钟)
"AppSecrets": {
"test123": "abcdefg", // AppKey与AppSecret的映射
"yourAppKey": "yourAppSecret" // 可以添加多个
}
}
```
## 使用方法
### 1. 标记需要签名验证的Controller或Action
在Controller类或Action方法上添加 `[RequireSignature]` 特性:
```csharp
using TrackCenter.Api.Filters;
// 整个Controller需要签名验证
[RequireSignature]
public class YourController : Controller
{
public IActionResult YourAction()
{
// ...
}
}
// 或者只对特定Action进行签名验证
public class YourController : Controller
{
[RequireSignature]
public IActionResult YourAction()
{
// ...
}
}
```
### 2. 客户端请求
客户端需要在HTTP Header中传递以下公共参数:
- **appKey** (String, 必填): 应用唯一标识,由平台分配
- **timestamp** (Long, 必填): Unix时间戳(秒),与服务器时间差不能超过5分钟
- **sign** (String, 必填): 签名结果
## 签名生成规则
### 步骤
1. **排序参数**: 按参数名排序:`appKey`, `timestamp`
2. **拼接字符串**: `appKey={value}timestamp={value}`
3. **拼接密钥**: `appKey={value}timestamp={value}&appSecret={value}`
4. **计算MD5**: 对拼接后的字符串计算MD5,并转为大写
### 示例
假设:
- appKey = `test123`
- timestamp = `1620000000`
- appSecret = `abcdefg`
生成步骤:
1. 排序后参数:`appKey`, `timestamp`
2. 拼接字符串:`appKey=test123timestamp=1620000000`
3. 拼接密钥:`appKey=test123timestamp=1620000000&appSecret=abcdefg`
4. 计算MD5并大写:`A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6`
### C# 客户端示例代码
```csharp
using System;
using System.Security.Cryptography;
using System.Text;
public class SignatureHelper
{
public static string GenerateSignature(string appKey, long timestamp, string appSecret)
{
// 按照参数名排序后拼接
var paramStr = $"appKey={appKey}timestamp={timestamp}";
// 拼接密钥
var signStr = $"{paramStr}&appSecret={appSecret}";
// 计算MD5并转大写
using var md5 = MD5.Create();
var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(signStr));
var signature = BitConverter.ToString(hashBytes).Replace("-", "").ToUpper();
return signature;
}
}
// 使用示例
var appKey = "test123";
var appSecret = "abcdefg";
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var sign = SignatureHelper.GenerateSignature(appKey, timestamp, appSecret);
// 在HTTP请求中添加Header
httpClient.DefaultRequestHeaders.Add("appKey", appKey);
httpClient.DefaultRequestHeaders.Add("timestamp", timestamp.ToString());
httpClient.DefaultRequestHeaders.Add("sign", sign);
```
## 错误响应
当签名验证失败时,API会返回以下格式的错误响应(状态码401):
```json
{
"code": 401,
"message": "错误描述"
}
```
可能的错误信息:
- `缺少appKey参数`
- `缺少或格式错误的timestamp参数`
- `缺少sign参数`
- `时间戳超出允许范围,请检查系统时间`
- `无效的appKey`
- `签名验证失败`
## 注意事项
1. **时间同步**: 确保客户端和服务器的时间同步,时间差不能超过5分钟
2. **HTTPS**: 建议在生产环境中使用HTTPS,防止中间人攻击
3. **AppSecret保密**: AppSecret必须妥善保管,不能泄露给第三方
4. **每次请求生成新签名**: 不要重复使用旧的签名,每次请求都应该使用当前时间戳生成新的签名

View File

@ -0,0 +1,139 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using TrackCenter.Domain.VEntities;
namespace TrackCenter.Application.Filters;
/// <summary>
/// 签名验证过滤器
/// </summary>
public class SignatureValidationFilter(
IOptions<SignatureValidationOptions> options,
ILogger<SignatureValidationFilter> logger,
IBasicRepository<VAppEntity> appRepository)
: IAsyncActionFilter
{
private readonly SignatureValidationOptions _options = options.Value;
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 检查是否启用签名验证
if (!_options.Enabled)
{
await next();
return;
}
// 检查是否标记为需要签名验证
var requiresSignature = context.ActionDescriptor.EndpointMetadata
.Any(m => m.GetType() == typeof(VerifySignatureAttribute));
if (!requiresSignature)
{
await next();
return;
}
var request = context.HttpContext.Request;
// 从Header中获取公共参数
if (!request.Headers.TryGetValue("appKey", out var appKey) || string.IsNullOrWhiteSpace(appKey))
{
context.Result = new JsonResult(new { code = 401, message = "缺少appKey参数" })
{
StatusCode = 401
};
return;
}
if (!request.Headers.TryGetValue("timestamp", out var timestampStr) ||
!long.TryParse(timestampStr, out var timestamp))
{
context.Result = new JsonResult(new { code = 401, message = "缺少或格式错误的timestamp参数" })
{
StatusCode = 401
};
return;
}
if (!request.Headers.TryGetValue("sign", out var sign) || string.IsNullOrWhiteSpace(sign))
{
context.Result = new JsonResult(new { code = 401, message = "缺少sign参数" })
{
StatusCode = 401
};
return;
}
// 验证时间戳是否在允许的范围内(5分钟)
var currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var timestampDiff = Math.Abs(currentTimestamp - timestamp);
if (timestampDiff > _options.MaxTimestampOffset)
{
logger.LogWarning("时间戳验证失败,appKey: {AppKey}, timestamp: {Timestamp}, 当前时间戳: {CurrentTimestamp}, 差异: {Diff}秒",
appKey, timestamp, currentTimestamp, timestampDiff);
context.Result = new JsonResult(new { code = 401, message = "时间戳超出允许范围,请检查系统时间" })
{
StatusCode = 401
};
return;
}
var appSecret = await appRepository.GetFirstAsync(x => x.AppKey == appKey, x => x.AppSecret);
if (appSecret.IsNullOrEmpty())
{
logger.LogWarning("无效的appKey: {AppKey}", appKey);
context.Result = new JsonResult(new { code = 401, message = "无效的appKey" })
{
StatusCode = 401
};
return;
}
// 生成签名并验证
var calculatedSign = GenerateSignature(appKey!, timestamp, appSecret!);
if (!string.Equals(calculatedSign, sign, StringComparison.OrdinalIgnoreCase))
{
logger.LogWarning("签名验证失败,appKey: {AppKey}, 期望签名: {Expected}, 实际签名: {Actual}",
appKey, calculatedSign, sign);
context.Result = new JsonResult(new { code = 401, message = "签名验证失败" })
{
StatusCode = 401
};
return;
}
// 验证通过,继续执行
await next();
}
/// <summary>
/// 生成签名
/// 规则:
/// 1. 排序后参数:appKey,timestamp,appSecret
/// 2. 用&拼接得到字符串:appKey={value}&timestamp={value}&appSecret={value}
/// 3. 计算MD5并大写,并去掉横杆-
/// </summary>
private string GenerateSignature(string appKey, long timestamp, string appSecret)
{
// 按照参数名排序后拼接
var paramStr = $"";
// 拼接密钥
var signStr = $"appKey={appKey}&timestamp={timestamp}&appSecret={appSecret}";
// 计算MD5并转大写
using var md5 = MD5.Create();
var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(signStr));
var signature = BitConverter.ToString(hashBytes).Replace("-", "").ToUpper();
return signature;
}
}

View File

@ -0,0 +1,17 @@
namespace TrackCenter.Application.Filters;
/// <summary>
/// 签名验证配置选项
/// </summary>
public class SignatureValidationOptions
{
/// <summary>
/// 是否启用签名验证
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// 时间戳允许的最大偏差(秒),默认300秒(5分钟)
/// </summary>
public int MaxTimestampOffset { get; set; } = 300;
}

View File

@ -0,0 +1,7 @@
namespace TrackCenter.Application.Filters;
/// <summary>
/// 标记接口需要进行签名验证
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class VerifySignatureAttribute : Attribute;

View File

@ -0,0 +1,98 @@
/*
* SignatureHelper 使用示例
*
* 根据API签名机制文档实现
*/
namespace TrackCenter.Application.Helpers;
/// <summary>
/// SignatureHelper 使用示例
/// </summary>
public static class SignatureHelperExample
{
/// <summary>
/// 生成签名示例
/// </summary>
public static void GenerateSignExample()
{
// 示例参数
string appKey = "test123";
long timestamp = 1620000000; // Unix时间戳(秒)
string appSecret = "abcdefg";
// 生成签名
string sign = SignatureHelper.GenerateSign(appKey, timestamp, appSecret);
// 输出: A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6 (示例结果)
Console.WriteLine($"生成的签名: {sign}");
}
/// <summary>
/// 验证签名示例
/// </summary>
public static void VerifySignExample()
{
string appKey = "test123";
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); // 当前时间戳
string appSecret = "abcdefg";
// 生成签名
string sign = SignatureHelper.GenerateSign(appKey, timestamp, appSecret);
// 验证签名
bool isValid = SignatureHelper.VerifySign(appKey, timestamp, sign, appSecret);
Console.WriteLine($"签名验证结果: {isValid}"); // 输出: true
}
/// <summary>
/// API请求示例
/// </summary>
public static async Task ApiRequestExample()
{
// 1. 准备参数
string appKey = "your_app_key";
string appSecret = "your_app_secret";
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
// 2. 生成签名
string sign = SignatureHelper.GenerateSign(appKey, timestamp, appSecret);
// 3. 构建请求
using var httpClient = new HttpClient();
var requestUrl = $"https://api.example.com/endpoint?appKey={appKey}&timestamp={timestamp}&sign={sign}";
// 4. 发送请求
var response = await httpClient.GetAsync(requestUrl);
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"响应内容: {content}");
}
/// <summary>
/// API验证示例(服务端)
/// </summary>
public static bool ValidateApiRequest(string appKey, long timestamp, string sign)
{
// 从数据库或配置获取appSecret
string appSecret = GetAppSecretFromDatabase(appKey);
// 验证签名
// 注意:必须使用当前时间戳,且与服务器时间差不超过5分钟
bool isValid = SignatureHelper.VerifySign(appKey, timestamp, sign, appSecret);
if (!isValid)
{
throw new UnauthorizedAccessException("签名验证失败");
}
return true;
}
private static string GetAppSecretFromDatabase(string appKey)
{
// 实际项目中应从数据库查询
return "abcdefg";
}
}

View File

@ -0,0 +1,74 @@
using System.Security.Cryptography;
using System.Text;
namespace TrackCenter.Application.Helpers;
/// <summary>
/// API签名工具类
/// </summary>
public static class SignatureHelper
{
/// <summary>
/// 生成API签名
/// </summary>
/// <param name="appKey">应用唯一标识</param>
/// <param name="timestamp">时间戳(Unix时间戳,单位:秒)</param>
/// <param name="appSecret">应用密钥</param>
/// <returns>MD5签名结果(大写)</returns>
public static string GenerateSign(string appKey, long timestamp, string appSecret)
{
// 1. 排序后拼接:appKey, timestamp
var sortedParams = $"appKey={appKey}&timestamp={timestamp}";
// 2. 拼接密钥:添加appSecret
var signString = $"{sortedParams}&appSecret={appSecret}";
// 3. 计算MD5并转大写
return CalculateMd5Hash(signString);
}
/// <summary>
/// 验证签名
/// </summary>
/// <param name="appKey">应用唯一标识</param>
/// <param name="timestamp">时间戳</param>
/// <param name="sign">待验证的签名</param>
/// <param name="appSecret">应用密钥</param>
/// <param name="maxTimeDifferenceMinutes">允许的最大时间差(分钟),默认5分钟</param>
/// <returns>签名是否有效</returns>
public static bool VerifySign(string appKey, long timestamp, string sign, string appSecret, int maxTimeDifferenceMinutes = 5)
{
// 1. 验证时间戳(防止重放攻击)
var currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var timeDifference = Math.Abs(currentTimestamp - timestamp);
if (timeDifference > maxTimeDifferenceMinutes * 60)
{
return false;
}
// 2. 生成签名并比较
var expectedSign = GenerateSign(appKey, timestamp, appSecret);
return string.Equals(expectedSign, sign, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// 计算MD5哈希值(大写)
/// </summary>
/// <param name="input">输入字符串</param>
/// <returns>MD5哈希值(大写)</returns>
private static string CalculateMd5Hash(string input)
{
using var md5 = MD5.Create();
var inputBytes = Encoding.UTF8.GetBytes(input);
var hashBytes = md5.ComputeHash(inputBytes);
var sb = new StringBuilder();
foreach (var b in hashBytes)
{
sb.Append(b.ToString("X2"));
}
return sb.ToString();
}
}

View File

@ -1,8 +1,11 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
namespace TrackCenter.Application.Services;
public class ApplicationServiceBase : ApplicationService
{
public int CurrentUserId => CurrentUser.GetId<int>();
public string? AppKey => LazyServiceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext?.Request.Headers["AppKey"];
}

View File

@ -31,10 +31,10 @@ public partial class OrderService
throw new BusinessException(message: $"订单:{input.OrderNo}不存在");
}
// tips:循环关联问题再消费时解决
if (input.OrderType == input.LinkOrderType)
// tips:2025-11-29 zzh 订单类型(1是小包,2是大包,4是主单),必填,同时orderType小于linkOrderType
if (input.OrderType < input.LinkOrderType)
{
throw new BusinessException(message: "不能同级关联");
throw new BusinessException(message: "orderType小于linkOrderType");
}
if (input.LinkOrderNos.Count < 1 || input.LinkOrderType == 0) return;

View File

@ -1,12 +1,13 @@
using System.ComponentModel.DataAnnotations;
using InterfaceForward.Domain.Shared.Helpers;
using Microsoft.AspNetCore.Components.Forms;
using TrackCenter.Application.Dtos.ProvideServices.V2;
using TrackCenter.Application.Filters;
using TrackCenter.Domain.Aggregates.OrderAggregate;
using TrackCenter.Domain.Aggregates.TrackPullAggregate;
using TrackCenter.Repository.Aggregates.OrderAggregate;
namespace TrackCenter.Application.Services.ProvideServices;
[VerifySignature]
public class OrderServiceV2(
IOrderRepository orderRepository,
IOrderOperationRecordEsRepository orderOperationRecordEsRepository,
@ -23,7 +24,7 @@ public class OrderServiceV2(
{
// 校验订单是否已存在,存在则更新
var order = await orderRepository.GetFirstAsync(x =>
x.AppKey == input.ApiCode && x.OrderType == TrackOrderType.小包 && x.OrderNo == input.OrderNo);
x.AppKey == AppKey && x.OrderType == TrackOrderType.小包 && x.OrderNo == input.OrderNo);
// 订单已存在则视为更新
if (order != null)
{
@ -35,7 +36,7 @@ public class OrderServiceV2(
{
OrderId = order.Id,
OrderNo = input.OrderNo,
OptionType = OrderOperationType.修改订单.ToString(),
OptionType = nameof(OrderOperationType.修改订单),
Content = input.ToJson(),
CreateUserId = CurrentUserId,
CreateUserName = CurrentUser.UserName ?? string.Empty,
@ -82,7 +83,7 @@ public class OrderServiceV2(
{
// 校验订单是否已存在,存在则更新
var order = await orderRepository.GetFirstAsync(x =>
x.AppKey == input.ApiCode && x.OrderType == TrackOrderType.大包 && x.OrderNo == input.BagNo);
x.AppKey == AppKey && x.OrderType == TrackOrderType.大包 && x.OrderNo == input.BagNo);
// 订单已存在则视为更新
if (order != null)
{
@ -131,11 +132,71 @@ public class OrderServiceV2(
};
}
/// <summary>
/// 新增、更新主单(存在就更新)
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost("OrderV2/UpsertMasterOrder")]
public async Task<IdBaseDto> UpsertAsync(UpsertMasterOrderInput input)
{
// 校验订单是否已存在,存在则更新
var order = await orderRepository.GetFirstAsync(x =>
x.AppKey == AppKey && x.OrderType == TrackOrderType.主单 && x.OrderNo == input.MasterNo);
// 订单已存在则视为更新
if (order != null)
{
order = await BuiltOrder(input);
await orderRepository.UpdateOrderAsync(order);
var index = new OrderOperationRecordIndex
{
OrderId = order.Id,
OrderNo = input.MasterNo,
OptionType = nameof(OrderOperationType.修改订单),
Content = input.ToJson(),
CreateUserId = CurrentUserId,
CreateUserName = CurrentUser.UserName ?? string.Empty,
};
_ = orderOperationRecordEsRepository.AddAsync(index);
return new IdBaseDto
{
Id = order.Id
};
}
order = await BuiltOrder(input);
var orderId = await orderRepository.CreateOrderAsync(order);
// 写入操作日志
var indexForAdd = new OrderOperationRecordIndex
{
OrderId = orderId,
OrderNo = input.MasterNo,
OptionType = nameof(OrderOperationType.创建订单),
Content = input.ToJson(),
CreateUserId = CurrentUserId,
CreateUserName = CurrentUser.UserName ?? string.Empty,
CreateTime = DateTime.Now
};
_ = orderOperationRecordEsRepository.AddAsync(indexForAdd);
return new IdBaseDto
{
Id = orderId
};
}
private async Task<OrderEntity> BuiltOrder(UpsertSmallBagInput input)
{
var order = new OrderEntity
{
AppKey = input.ApiCode,
AppKey = AppKey!,
ServiceProviderCode = input.ApiCode,
OrderType = TrackOrderType.小包,
OrderNo = input.OrderNo,
TrackNo = input.TrackNo,
@ -166,8 +227,8 @@ public class OrderServiceV2(
{
var order = new OrderEntity
{
AppKey = input.ApiCode,
ServiceProviderCode = input.ApiCode,
AppKey = AppKey!,
//ServiceProviderCode = input.ApiCode,
OrderType = TrackOrderType.大包,
OrderNo = input.BagNo,
ThirdPartyOrderNumber = input.OtherNo,
@ -190,6 +251,32 @@ public class OrderServiceV2(
return Task.FromResult(order);
}
private async Task<OrderEntity> BuiltOrder(UpsertMasterOrderInput input)
{
var order = new OrderEntity
{
AppKey = AppKey!,
ServiceProviderCode = input.ApiCode,
OrderType = TrackOrderType.小包,
OrderNo = input.MasterNo,
IsTransit = input.IsTransfer,
ArrivalCountry = input.ArrivalCountry,
LeaveCountry = input.LeaveCountry,
};
order.OrderFlights = new List<OrderFlightEntity>();
var config = await trackPullConfigRepository
.FirstOrDefaultAsync(x => x.AppKey == input.ApiCode
&& x.ServiceProviderCode == input.ApiCode);
if (config == null) throw new BusinessException(message: "没有一个拉取配置可以适用,请配置,否则无法拉取轨迹");
order.OrderPullStatus = [CreateOrderPullStatus(config, order)];
return order;
}
private static OrderPullStatusEntity CreateOrderPullStatus(TrackPullConfigEntity config, OrderEntity order)
{
var status = new OrderPullStatusEntity
@ -219,124 +306,3 @@ public class OrderServiceV2(
return status;
}
}
public class UpsertSmallBagInput
{
/// <summary>
/// 客户名称,或者code,必填
/// </summary>
[Required]
public string Client { get; set; }
/// <summary>
/// 客户单号,必填
/// </summary>
[Required]
public string OrderNo { get; set; }
/// <summary>
/// 尾程单号,必填
/// </summary>
[Required]
public string TrackNo { get; set; }
/// <summary>
/// 其他单号
/// </summary>
public string OtherNo { get; set; }
/// <summary>
/// 渠道名称,或者code
/// </summary>
public string Channel { get; set; }
/// <summary>
/// 产品名称,或者code
/// </summary>
public string Product { get; set; }
/// <summary>
/// 服务商code
/// </summary>
public string ApiCode { get; set; }
/// <summary>
/// 收件人信息
/// </summary>
public Consignee Consignee { get; set; }
}
public class Consignee
{
/// <summary>
/// 收件人国家,必填
/// </summary>
[Required]
public string CountryCode { get; set; }
/// <summary>
/// 收件人州省
/// </summary>
public string Province { get; set; }
/// <summary>
/// 收件人邮编
/// </summary>
public string Postcode { get; set; }
/// <summary>
/// 收件人城市
/// </summary>
public string City { get; set; }
/// <summary>
/// 收件人邮箱
/// </summary>
public string Email { get; set; }
}
public class UpsertBigBagInput
{
/// <summary>
/// 客户名称,或者code,必填
/// </summary>
[Required]
public string Client { get; set; }
/// <summary>
/// 客户单号,必填
/// </summary>
[Required]
public string BagNo { get; set; }
/// <summary>
/// 其他单号
/// </summary>
public string OtherNo { get; set; }
/// <summary>
/// 渠道名称,或者code
/// </summary>
public string Channel { get; set; }
/// <summary>
/// 产品名称,或者code
/// </summary>
public string Product { get; set; }
/// <summary>
/// 产品名称,或者code
/// </summary>
public string Country { get; set; }
/// <summary>
/// 服务商code
/// </summary>
public string ApiCode { get; set; }
}
public class UpsertMsterOrderInput
{
}