fix(track): UpsertSmallBag auth params in query, align sign helper

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-06-29 12:18:57 +08:00
parent 545f0825f8
commit e642f089ba
5 changed files with 49 additions and 36 deletions

View File

@ -24,9 +24,6 @@ $timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$sign = Get-SmallBagSign -Key $AppKey -Ts $timestamp -Secret $AppSecret $sign = Get-SmallBagSign -Key $AppKey -Ts $timestamp -Secret $AppSecret
$body = @{ $body = @{
appKey = $AppKey
timestamp = $timestamp
sign = $sign
client = $Client client = $Client
orderNo = $OrderNo orderNo = $OrderNo
trackNo = "" trackNo = ""
@ -43,7 +40,8 @@ $body = @{
} }
} | ConvertTo-Json -Depth 5 -Compress } | ConvertTo-Json -Depth 5 -Compress
$url = ($BaseUrl.TrimEnd('/')) + "/OrderV2/UpsertSmallBag" $base = $BaseUrl.TrimEnd('/')
$url = "$base/OrderV2/UpsertSmallBag?appKey=$([uri]::EscapeDataString($AppKey))&timestamp=$timestamp&sign=$([uri]::EscapeDataString($sign))"
Write-Host "========== UpsertSmallBag Debug ==========" -ForegroundColor Cyan Write-Host "========== UpsertSmallBag Debug ==========" -ForegroundColor Cyan
Write-Host "URL: $url" Write-Host "URL: $url"
@ -66,7 +64,7 @@ catch {
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$errBody = $reader.ReadToEnd() $errBody = $reader.ReadToEnd()
Write-Host "HTTP $([int]$_.Exception.Response.StatusCode)" -ForegroundColor Red Write-Host "HTTP $([int]$_.Exception.Response.StatusCode)" -ForegroundColor Red
if ($errBody) { Write-Host $errBody } else { Write-Host "(empty body — 多为签名/鉴权失败,请核对 appKey/appSecret 或联系轨迹平台)" } if ($errBody) { Write-Host $errBody } else { Write-Host "(empty body — 请核对 appKey/appSecret 或联系轨迹平台)" }
} }
else { else {
Write-Host $_.Exception.Message -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red

View File

@ -66,15 +66,15 @@ internal sealed class SmallBagTrackOrderClient(
var appKey = settings.AppKey.Trim(); var appKey = settings.AppKey.Trim();
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var sign = SmallBagTrackSignHelper.GenerateSign(appKey, timestamp, settings.AppSecret.Trim()); var sign = SmallBagTrackSignHelper.GenerateSign(appKey, timestamp, settings.AppSecret.Trim());
var payload = SmallBagTrackUpsertApiRequest.FromBusinessRequest(request, appKey, timestamp, sign); var body = SmallBagTrackUpsertApiRequest.FromBusinessRequest(request);
var url = $"{settings.BaseUrl.Trim().TrimEnd('/')}/OrderV2/UpsertSmallBag"; var url = SmallBagTrackUpsertApiRequest.BuildRequestUrl(settings.BaseUrl, appKey, timestamp, sign);
var requestJson = JsonSerializer.Serialize(payload, JsonOptions); var requestJson = JsonSerializer.Serialize(body, JsonOptions);
var client = httpClientFactory.CreateClient(HttpClientName); var client = httpClientFactory.CreateClient(HttpClientName);
client.Timeout = TimeSpan.FromSeconds(Math.Max(5, settings.TimeoutSeconds)); client.Timeout = TimeSpan.FromSeconds(Math.Max(5, settings.TimeoutSeconds));
using var response = await client.PostAsJsonAsync(url, payload, cancellationToken); using var response = await client.PostAsJsonAsync(url, body, cancellationToken);
var body = await response.Content.ReadAsStringAsync(cancellationToken); var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
return new SmallBagTrackUpsertResult return new SmallBagTrackUpsertResult
{ {
Skipped = false, Skipped = false,
@ -82,7 +82,7 @@ internal sealed class SmallBagTrackOrderClient(
Url = url, Url = url,
RequestJson = requestJson, RequestJson = requestJson,
StatusCode = (int)response.StatusCode, StatusCode = (int)response.StatusCode,
ResponseBody = body ResponseBody = responseBody
}; };
} }

View File

@ -4,13 +4,26 @@ using System.Text;
namespace Oms.Application.Integrations.SmallBagTrack; namespace Oms.Application.Integrations.SmallBagTrack;
/// <summary> /// <summary>
/// 轨迹服务 API 签名:appKey={}&amp;timestamp={}&amp;appSecret={} → MD5 大写。 /// 轨迹服务 API 签名(与轨迹平台官方算法一致)。
/// </summary> /// </summary>
internal static class SmallBagTrackSignHelper internal static class SmallBagTrackSignHelper
{ {
/// <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) public static string GenerateSign(string appKey, long timestamp, string appSecret)
{ {
var signString = $"appKey={appKey}&timestamp={timestamp}&appSecret={appSecret}"; // 1. 排序后拼接:appKey, timestamp
var sortedParams = $"appKey={appKey}&timestamp={timestamp}";
// 2. 拼接密钥
var signString = $"{sortedParams}&appSecret={appSecret}";
// 3. MD5 大写
return CalculateMd5Hash(signString); return CalculateMd5Hash(signString);
} }

View File

@ -1,16 +1,10 @@
namespace Oms.Application.Integrations.SmallBagTrack; namespace Oms.Application.Integrations.SmallBagTrack;
/// <summary> /// <summary>
/// POST /OrderV2/UpsertSmallBag 完整请求体(含公共参数 + 业务字段)。 /// POST /OrderV2/UpsertSmallBag 业务请求体(公共参数 appKey/timestamp/sign 走 Query)。
/// </summary> /// </summary>
internal sealed class SmallBagTrackUpsertApiRequest internal sealed class SmallBagTrackUpsertApiRequest
{ {
public string AppKey { get; set; } = string.Empty;
public long Timestamp { get; set; }
public string Sign { get; set; } = string.Empty;
public string Client { get; set; } = string.Empty; public string Client { get; set; } = string.Empty;
public string OrderNo { get; set; } = string.Empty; public string OrderNo { get; set; } = string.Empty;
@ -27,16 +21,9 @@ internal sealed class SmallBagTrackUpsertApiRequest
public SmallBagTrackConsigneeRequest Consignee { get; set; } = new(); public SmallBagTrackConsigneeRequest Consignee { get; set; } = new();
public static SmallBagTrackUpsertApiRequest FromBusinessRequest( public static SmallBagTrackUpsertApiRequest FromBusinessRequest(SmallBagTrackUpsertRequest request) =>
SmallBagTrackUpsertRequest request,
string appKey,
long timestamp,
string sign) =>
new() new()
{ {
AppKey = appKey,
Timestamp = timestamp,
Sign = sign,
Client = request.Client, Client = request.Client,
OrderNo = request.OrderNo, OrderNo = request.OrderNo,
TrackNo = request.TrackNo, TrackNo = request.TrackNo,
@ -46,4 +33,15 @@ internal sealed class SmallBagTrackUpsertApiRequest
ApiCode = request.ApiCode, ApiCode = request.ApiCode,
Consignee = request.Consignee Consignee = request.Consignee
}; };
public static string BuildRequestUrl(string baseUrl, string appKey, long timestamp, string sign)
{
var root = $"{baseUrl.Trim().TrimEnd('/')}/OrderV2/UpsertSmallBag";
var query = string.Join(
'&',
$"appKey={Uri.EscapeDataString(appKey)}",
$"timestamp={timestamp}",
$"sign={Uri.EscapeDataString(sign)}");
return $"{root}?{query}";
}
} }

View File

@ -5,15 +5,19 @@ namespace Oms.Tests;
public class SmallBagTrackSignHelperTests public class SmallBagTrackSignHelperTests
{ {
[Fact] private const string AppKey = "W2606001";
public void GenerateSign_ShouldMatchMd5UppercaseRule() private const string AppSecret = "ziK8UzlQ39m3kdL346XEBg8nljY8os52";
[Theory]
[InlineData(1700000000, "A744D764DEE7C925AD6750257A4DEEE7")]
[InlineData(1782705351, "6C76CF34DFB53A0686C7C97284133BB7")]
[InlineData(1782705489, "E9633AB5419AB15D95D38903E8001E2D")]
public void GenerateSign_ShouldMatchOfficialMd5UppercaseRule(long timestamp, string expected)
{ {
const string appKey = "W2606001"; var signString = $"appKey={AppKey}&timestamp={timestamp}&appSecret={AppSecret}";
const long timestamp = 1700000000;
const string appSecret = "ziK8UzlQ39m3kdL346XEBg8nljY8os52";
var sign = SmallBagTrackSignHelper.GenerateSign(appKey, timestamp, appSecret); SmallBagTrackSignHelper.GenerateSign(AppKey, timestamp, AppSecret)
.Should()
sign.Should().Be("A744D764DEE7C925AD6750257A4DEEE7"); .Be(expected, because: $"signString={signString}");
} }
} }