t1
This commit is contained in:
parent
7b3f083b9b
commit
1966976944
@ -15,7 +15,7 @@ using TrackCenter.Domain.Extensions;
|
|||||||
using TrackCenter.Repository;
|
using TrackCenter.Repository;
|
||||||
using TrackCenter.Repository.Otel;
|
using TrackCenter.Repository.Otel;
|
||||||
using TrackCenter.Repository.Otel.Options;
|
using TrackCenter.Repository.Otel.Options;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using TrackCenter.Application.Filters;
|
||||||
|
|
||||||
namespace TrackCenter.Api;
|
namespace TrackCenter.Api;
|
||||||
|
|
||||||
@ -37,6 +37,9 @@ public class TrackCenterApiModule : FakeModule
|
|||||||
options.ScanApplicationServices<TrackCenterApplicationModule>();
|
options.ScanApplicationServices<TrackCenterApplicationModule>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 配置签名验证选项
|
||||||
|
services.Configure<SignatureValidationOptions>(configuration.GetSection("SignatureValidation"));
|
||||||
|
|
||||||
// 全局授权配置:所有接口默认需要授权
|
// 全局授权配置:所有接口默认需要授权
|
||||||
services.AddMvc(options =>
|
services.AddMvc(options =>
|
||||||
{
|
{
|
||||||
@ -44,6 +47,8 @@ public class TrackCenterApiModule : FakeModule
|
|||||||
.RequireAuthenticatedUser()
|
.RequireAuthenticatedUser()
|
||||||
.Build();
|
.Build();
|
||||||
options.Filters.Add(new AuthorizeFilter(policy));
|
options.Filters.Add(new AuthorizeFilter(policy));
|
||||||
|
// 添加签名验证过滤器
|
||||||
|
options.Filters.Add<SignatureValidationFilter>();
|
||||||
}).AddNewtonsoftJson();
|
}).AddNewtonsoftJson();
|
||||||
|
|
||||||
services.AddFakeExceptionFilter()
|
services.AddFakeExceptionFilter()
|
||||||
|
|||||||
@ -187,5 +187,9 @@
|
|||||||
"ExpiredTime": 50000, // 过期时间,long 类型,单位分钟,默认20分钟
|
"ExpiredTime": 50000, // 过期时间,long 类型,单位分钟,默认20分钟
|
||||||
"ClockSkew": 10, // 过期时间容错值,long 类型,单位秒,默认5秒
|
"ClockSkew": 10, // 过期时间容错值,long 类型,单位秒,默认5秒
|
||||||
"Algorithm": "HS256" // 加密算法,string 类型,默认 HS256
|
"Algorithm": "HS256" // 加密算法,string 类型,默认 HS256
|
||||||
|
},
|
||||||
|
"SignatureValidation": {
|
||||||
|
"Enabled": true, // 是否启用签名验证
|
||||||
|
"MaxTimestampOffset": 300 // 时间戳允许的最大偏差(秒),默认300秒(5分钟)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
189
src/TrackCenter.Application/Dtos/ProvideServices/V2/OrderDtos.cs
Normal file
189
src/TrackCenter.Application/Dtos/ProvideServices/V2/OrderDtos.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
|
||||||
145
src/TrackCenter.Application/Filters/README.md
Normal file
145
src/TrackCenter.Application/Filters/README.md
Normal 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. **每次请求生成新签名**: 不要重复使用旧的签名,每次请求都应该使用当前时间戳生成新的签名
|
||||||
139
src/TrackCenter.Application/Filters/SignatureValidationFilter.cs
Normal file
139
src/TrackCenter.Application/Filters/SignatureValidationFilter.cs
Normal 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}×tamp={value}&appSecret={value}
|
||||||
|
/// 3. 计算MD5并大写,并去掉横杆-
|
||||||
|
/// </summary>
|
||||||
|
private string GenerateSignature(string appKey, long timestamp, string appSecret)
|
||||||
|
{
|
||||||
|
// 按照参数名排序后拼接
|
||||||
|
var paramStr = $"";
|
||||||
|
|
||||||
|
// 拼接密钥
|
||||||
|
var signStr = $"appKey={appKey}×tamp={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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
namespace TrackCenter.Application.Filters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记接口需要进行签名验证
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class VerifySignatureAttribute : Attribute;
|
||||||
@ -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}×tamp={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";
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/TrackCenter.Application/Helpers/SignatureHelper.cs
Normal file
74
src/TrackCenter.Application/Helpers/SignatureHelper.cs
Normal 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}×tamp={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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,11 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace TrackCenter.Application.Services;
|
namespace TrackCenter.Application.Services;
|
||||||
|
|
||||||
public class ApplicationServiceBase : ApplicationService
|
public class ApplicationServiceBase : ApplicationService
|
||||||
{
|
{
|
||||||
public int CurrentUserId => CurrentUser.GetId<int>();
|
public int CurrentUserId => CurrentUser.GetId<int>();
|
||||||
|
|
||||||
|
public string? AppKey => LazyServiceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext?.Request.Headers["AppKey"];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,10 +31,10 @@ public partial class OrderService
|
|||||||
throw new BusinessException(message: $"订单:{input.OrderNo}不存在");
|
throw new BusinessException(message: $"订单:{input.OrderNo}不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// tips:循环关联问题再消费时解决
|
// tips:2025-11-29 zzh 订单类型(1是小包,2是大包,4是主单),必填,同时orderType小于linkOrderType
|
||||||
if (input.OrderType == input.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;
|
if (input.LinkOrderNos.Count < 1 || input.LinkOrderType == 0) return;
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using InterfaceForward.Domain.Shared.Helpers;
|
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.OrderAggregate;
|
||||||
using TrackCenter.Domain.Aggregates.TrackPullAggregate;
|
using TrackCenter.Domain.Aggregates.TrackPullAggregate;
|
||||||
using TrackCenter.Repository.Aggregates.OrderAggregate;
|
using TrackCenter.Repository.Aggregates.OrderAggregate;
|
||||||
|
|
||||||
namespace TrackCenter.Application.Services.ProvideServices;
|
namespace TrackCenter.Application.Services.ProvideServices;
|
||||||
|
|
||||||
|
[VerifySignature]
|
||||||
public class OrderServiceV2(
|
public class OrderServiceV2(
|
||||||
IOrderRepository orderRepository,
|
IOrderRepository orderRepository,
|
||||||
IOrderOperationRecordEsRepository orderOperationRecordEsRepository,
|
IOrderOperationRecordEsRepository orderOperationRecordEsRepository,
|
||||||
@ -23,7 +24,7 @@ public class OrderServiceV2(
|
|||||||
{
|
{
|
||||||
// 校验订单是否已存在,存在则更新
|
// 校验订单是否已存在,存在则更新
|
||||||
var order = await orderRepository.GetFirstAsync(x =>
|
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)
|
if (order != null)
|
||||||
{
|
{
|
||||||
@ -35,7 +36,7 @@ public class OrderServiceV2(
|
|||||||
{
|
{
|
||||||
OrderId = order.Id,
|
OrderId = order.Id,
|
||||||
OrderNo = input.OrderNo,
|
OrderNo = input.OrderNo,
|
||||||
OptionType = OrderOperationType.修改订单.ToString(),
|
OptionType = nameof(OrderOperationType.修改订单),
|
||||||
Content = input.ToJson(),
|
Content = input.ToJson(),
|
||||||
CreateUserId = CurrentUserId,
|
CreateUserId = CurrentUserId,
|
||||||
CreateUserName = CurrentUser.UserName ?? string.Empty,
|
CreateUserName = CurrentUser.UserName ?? string.Empty,
|
||||||
@ -82,7 +83,7 @@ public class OrderServiceV2(
|
|||||||
{
|
{
|
||||||
// 校验订单是否已存在,存在则更新
|
// 校验订单是否已存在,存在则更新
|
||||||
var order = await orderRepository.GetFirstAsync(x =>
|
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)
|
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)
|
private async Task<OrderEntity> BuiltOrder(UpsertSmallBagInput input)
|
||||||
{
|
{
|
||||||
var order = new OrderEntity
|
var order = new OrderEntity
|
||||||
{
|
{
|
||||||
AppKey = input.ApiCode,
|
AppKey = AppKey!,
|
||||||
|
ServiceProviderCode = input.ApiCode,
|
||||||
OrderType = TrackOrderType.小包,
|
OrderType = TrackOrderType.小包,
|
||||||
OrderNo = input.OrderNo,
|
OrderNo = input.OrderNo,
|
||||||
TrackNo = input.TrackNo,
|
TrackNo = input.TrackNo,
|
||||||
@ -166,8 +227,8 @@ public class OrderServiceV2(
|
|||||||
{
|
{
|
||||||
var order = new OrderEntity
|
var order = new OrderEntity
|
||||||
{
|
{
|
||||||
AppKey = input.ApiCode,
|
AppKey = AppKey!,
|
||||||
ServiceProviderCode = input.ApiCode,
|
//ServiceProviderCode = input.ApiCode,
|
||||||
OrderType = TrackOrderType.大包,
|
OrderType = TrackOrderType.大包,
|
||||||
OrderNo = input.BagNo,
|
OrderNo = input.BagNo,
|
||||||
ThirdPartyOrderNumber = input.OtherNo,
|
ThirdPartyOrderNumber = input.OtherNo,
|
||||||
@ -190,6 +251,32 @@ public class OrderServiceV2(
|
|||||||
return Task.FromResult(order);
|
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)
|
private static OrderPullStatusEntity CreateOrderPullStatus(TrackPullConfigEntity config, OrderEntity order)
|
||||||
{
|
{
|
||||||
var status = new OrderPullStatusEntity
|
var status = new OrderPullStatusEntity
|
||||||
@ -219,124 +306,3 @@ public class OrderServiceV2(
|
|||||||
return status;
|
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
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user