diff --git a/db.sql b/db.sql
index 3704730..c12be5c 100644
--- a/db.sql
+++ b/db.sql
@@ -104,6 +104,7 @@ CREATE TABLE `t_order` (
`ServiceNo` VARCHAR(64) NOT NULL COMMENT '运单号',
`ClientNo` VARCHAR(64) NULL COMMENT '客户单号',
`TrackNo` VARCHAR(64) NULL COMMENT '跟踪号',
+ `DownstreamServiceNo` VARCHAR(64) NULL COMMENT '服务商单号(下游 createOrderOne Data.serviceNo)',
`ProductCode` VARCHAR(128) NULL COMMENT '产品编码',
`ChannelCode` VARCHAR(128) NULL COMMENT '渠道编码',
`DownstreamSupplierCode` VARCHAR(64) NULL COMMENT '下游 API 服务商编码 supplierCode',
diff --git a/sql/20260728_order_downstream_service_no.sql b/sql/20260728_order_downstream_service_no.sql
new file mode 100644
index 0000000..691807f
--- /dev/null
+++ b/sql/20260728_order_downstream_service_no.sql
@@ -0,0 +1,5 @@
+-- Add DownstreamServiceNo to t_order (provider service no from createOrderOne Data.serviceNo)
+-- Backup production DB before running.
+
+ALTER TABLE `t_order`
+ ADD COLUMN `DownstreamServiceNo` VARCHAR(64) NULL COMMENT 'Downstream provider service no' AFTER `TrackNo`;
diff --git a/src/Oms.Application/Dtos/Orders/OrderDownstreamForwardDtos.cs b/src/Oms.Application/Dtos/Orders/OrderDownstreamForwardDtos.cs
index ecd872d..472212e 100644
--- a/src/Oms.Application/Dtos/Orders/OrderDownstreamForwardDtos.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderDownstreamForwardDtos.cs
@@ -32,4 +32,14 @@ public class OrderDownstreamForwardResult
public int? StatusCode { get; set; }
public string ResponseBody { get; set; } = string.Empty;
+
+ ///
+ /// 回写后的服务商单号(Data.serviceNo)。
+ ///
+ public string? DownstreamServiceNo { get; set; }
+
+ ///
+ /// 回写后的跟踪号(Data.trackNo)。
+ ///
+ public string? TrackNo { get; set; }
}
diff --git a/src/Oms.Application/Dtos/Orders/OrderOutput.cs b/src/Oms.Application/Dtos/Orders/OrderOutput.cs
index d956091..7181b83 100644
--- a/src/Oms.Application/Dtos/Orders/OrderOutput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderOutput.cs
@@ -20,6 +20,11 @@ public class OrderOutput
///
public string? TrackNo { get; set; }
+ ///
+ /// 服务商单号(下游 createOrderOne Data.serviceNo)。
+ ///
+ public string? DownstreamServiceNo { get; set; }
+
///
/// 服务单号。
///
diff --git a/src/Oms.Application/Dtos/Orders/OrderPaginatedOutput.cs b/src/Oms.Application/Dtos/Orders/OrderPaginatedOutput.cs
index ca5a6fe..6aeb931 100644
--- a/src/Oms.Application/Dtos/Orders/OrderPaginatedOutput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderPaginatedOutput.cs
@@ -25,6 +25,11 @@ public class OrderPaginatedOutput
///
public string? TrackNo { get; set; }
+ ///
+ /// 服务商单号(下游 createOrderOne Data.serviceNo)。
+ ///
+ public string? DownstreamServiceNo { get; set; }
+
///
/// 产品编码。
///
diff --git a/src/Oms.Application/Integrations/Itfx/ItfxCreateOrderOneDataParser.cs b/src/Oms.Application/Integrations/Itfx/ItfxCreateOrderOneDataParser.cs
new file mode 100644
index 0000000..c0eb1a2
--- /dev/null
+++ b/src/Oms.Application/Integrations/Itfx/ItfxCreateOrderOneDataParser.cs
@@ -0,0 +1,145 @@
+using System.Text.Json;
+using Oms.Domain.OrdersAggregate.Entities;
+
+namespace Oms.Application.Integrations.Itfx;
+
+///
+/// 解析 itfx createOrderOne 成功响应中的 Data 字段,并回写订单。
+///
+internal static class ItfxCreateOrderOneDataParser
+{
+ public sealed class CreateOrderOneData
+ {
+ public string? ServiceNo { get; init; }
+
+ public string? TrackNo { get; init; }
+
+ public string? ClientNo { get; init; }
+
+ public string? LabelUrl { get; init; }
+ }
+
+ public static bool TryParse(string? responseBody, out CreateOrderOneData? data)
+ {
+ data = null;
+ if (string.IsNullOrWhiteSpace(responseBody))
+ {
+ return false;
+ }
+
+ try
+ {
+ using var doc = JsonDocument.Parse(responseBody);
+ var root = doc.RootElement;
+ if (root.ValueKind != JsonValueKind.Object)
+ {
+ return false;
+ }
+
+ if (!TryGetPropertyIgnoreCase(root, out var dataElement, "data", "Data")
+ || dataElement.ValueKind != JsonValueKind.Object)
+ {
+ return false;
+ }
+
+ data = new CreateOrderOneData
+ {
+ ServiceNo = ReadString(dataElement, "serviceNo", "ServiceNo"),
+ TrackNo = ReadString(dataElement, "trackNo", "TrackNo"),
+ ClientNo = ReadString(dataElement, "clientNo", "ClientNo"),
+ LabelUrl = ReadString(dataElement, "labelUrl", "LabelUrl"),
+ };
+ return !string.IsNullOrWhiteSpace(data.ServiceNo)
+ || !string.IsNullOrWhiteSpace(data.TrackNo)
+ || !string.IsNullOrWhiteSpace(data.ClientNo)
+ || !string.IsNullOrWhiteSpace(data.LabelUrl);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// 将下游返回写入订单:服务商单号 ← Data.serviceNo,跟踪号 ← Data.trackNo。
+ /// 空值不覆盖已有字段。
+ ///
+ public static bool ApplyToOrder(Order order, CreateOrderOneData data)
+ {
+ var changed = false;
+
+ if (!string.IsNullOrWhiteSpace(data.ServiceNo))
+ {
+ var value = data.ServiceNo.Trim();
+ if (!string.Equals(order.DownstreamServiceNo, value, StringComparison.Ordinal))
+ {
+ order.DownstreamServiceNo = value;
+ changed = true;
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(data.TrackNo))
+ {
+ var value = data.TrackNo.Trim();
+ if (!string.Equals(order.TrackNo, value, StringComparison.Ordinal))
+ {
+ order.TrackNo = value;
+ changed = true;
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(data.LabelUrl))
+ {
+ var value = data.LabelUrl.Trim();
+ if (!string.Equals(order.LabelUrl, value, StringComparison.Ordinal))
+ {
+ order.LabelUrl = value;
+ changed = true;
+ }
+ }
+
+ return changed;
+ }
+
+ private static bool TryGetPropertyIgnoreCase(JsonElement root, out JsonElement value, params string[] names)
+ {
+ foreach (var name in names)
+ {
+ if (root.TryGetProperty(name, out value))
+ {
+ return true;
+ }
+ }
+
+ foreach (var prop in root.EnumerateObject())
+ {
+ foreach (var name in names)
+ {
+ if (prop.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
+ {
+ value = prop.Value;
+ return true;
+ }
+ }
+ }
+
+ value = default;
+ return false;
+ }
+
+ private static string? ReadString(JsonElement root, params string[] names)
+ {
+ if (!TryGetPropertyIgnoreCase(root, out var prop, names))
+ {
+ return null;
+ }
+
+ return prop.ValueKind switch
+ {
+ JsonValueKind.String => prop.GetString()?.Trim(),
+ JsonValueKind.Number => prop.GetRawText(),
+ JsonValueKind.Null => null,
+ _ => null
+ };
+ }
+}
diff --git a/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs b/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs
index 2114ffa..e16f6ff 100644
--- a/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs
+++ b/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs
@@ -51,12 +51,19 @@ internal sealed class ItfxForwardClient(
using var response = await client.SendAsync(request, cancellationToken);
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
+ var statusCode = (int)response.StatusCode;
+ var success = ItfxForwardResultEvaluator.IsBusinessSuccess(
+ statusCode,
+ responseBody,
+ out _,
+ out _);
+
return new ItfxForwardResult
{
- Success = response.IsSuccessStatusCode,
+ Success = success,
Url = url,
RequestJson = requestJson,
- StatusCode = (int)response.StatusCode,
+ StatusCode = statusCode,
ResponseBody = responseBody
};
}
diff --git a/src/Oms.Application/Integrations/Itfx/ItfxForwardResultEvaluator.cs b/src/Oms.Application/Integrations/Itfx/ItfxForwardResultEvaluator.cs
new file mode 100644
index 0000000..196fdd2
--- /dev/null
+++ b/src/Oms.Application/Integrations/Itfx/ItfxForwardResultEvaluator.cs
@@ -0,0 +1,115 @@
+using System.Text.Json;
+
+namespace Oms.Application.Integrations.Itfx;
+
+///
+/// itfx ForwardWithSubscribe 业务成败判定。
+/// 注意:业务失败时常仍返回 HTTP 200,body.code=400。
+///
+internal static class ItfxForwardResultEvaluator
+{
+ public static bool IsBusinessSuccess(int? httpStatusCode, string? responseBody, out string? businessCode, out string? businessMessage)
+ {
+ businessCode = null;
+ businessMessage = null;
+ var httpOk = httpStatusCode is >= 200 and < 300;
+
+ if (TryReadBusinessFields(responseBody, out businessCode, out businessMessage))
+ {
+ if (!IsSuccessCode(businessCode))
+ {
+ return false;
+ }
+
+ // body.code 成功时仍要求 HTTP 2xx(若有状态码)
+ return httpOk || httpStatusCode is null;
+ }
+
+ return httpOk;
+ }
+
+ public static bool IsSuccessCode(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code))
+ {
+ return true;
+ }
+
+ var normalized = code.Trim();
+ return normalized is "0" or "200"
+ || normalized.Equals("OK", StringComparison.OrdinalIgnoreCase)
+ || normalized.Equals("Success", StringComparison.OrdinalIgnoreCase)
+ || normalized.Equals("true", StringComparison.OrdinalIgnoreCase);
+ }
+
+ public static string BuildFailReason(int? httpStatusCode, string? responseBody, string? businessCode, string? businessMessage)
+ {
+ if (!string.IsNullOrWhiteSpace(businessMessage))
+ {
+ var codePart = string.IsNullOrWhiteSpace(businessCode) ? "" : $"code={businessCode} ";
+ return $"下游预报失败: {codePart}{businessMessage.Trim()}";
+ }
+
+ if (!string.IsNullOrWhiteSpace(responseBody))
+ {
+ var preview = responseBody.Length > 500 ? responseBody[..500] + "…" : responseBody;
+ return $"下游预报失败: HTTP {httpStatusCode} {preview}";
+ }
+
+ return $"下游预报失败: HTTP {httpStatusCode}";
+ }
+
+ private static bool TryReadBusinessFields(string? responseBody, out string? code, out string? message)
+ {
+ code = null;
+ message = null;
+ if (string.IsNullOrWhiteSpace(responseBody))
+ {
+ return false;
+ }
+
+ try
+ {
+ using var doc = JsonDocument.Parse(responseBody);
+ var root = doc.RootElement;
+ if (root.ValueKind != JsonValueKind.Object)
+ {
+ return false;
+ }
+
+ code = ReadString(root, "code", "Code");
+ message = ReadString(root, "msg", "Msg", "message", "Message");
+ return !string.IsNullOrWhiteSpace(code) || !string.IsNullOrWhiteSpace(message);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static string? ReadString(JsonElement root, params string[] names)
+ {
+ foreach (var name in names)
+ {
+ if (!root.TryGetProperty(name, out var prop))
+ {
+ continue;
+ }
+
+ var value = prop.ValueKind switch
+ {
+ JsonValueKind.String => prop.GetString(),
+ JsonValueKind.Number => prop.GetRawText(),
+ JsonValueKind.True => "true",
+ JsonValueKind.False => "false",
+ _ => null
+ };
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ return value.Trim();
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs
index 3059463..b97d847 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs
@@ -44,15 +44,6 @@ internal sealed class SmallBagTrackOrderClient(
}
logger.LogInformation("UpsertSmallBag succeeded for order {OrderNo}", request.OrderNo);
-
- if (request.AppendInitialTrackOnSuccess)
- {
- await AppendLifecycleTrackAsync(
- request.OrderNo,
- SmallBagTrackLifecycleEvents.CreateTrackCode,
- request.Authorization,
- cancellationToken);
- }
}
public Task AppendLifecycleTrackAsync(
@@ -105,7 +96,7 @@ internal sealed class SmallBagTrackOrderClient(
body,
JsonOptions);
- return new SmallBagTrackUpsertResult
+ var result = new SmallBagTrackUpsertResult
{
Skipped = false,
Success = response.Response.IsSuccessStatusCode,
@@ -115,6 +106,18 @@ internal sealed class SmallBagTrackOrderClient(
StatusCode = (int)response.Response.StatusCode,
ResponseBody = response.ResponseBody
};
+
+ // 创单成功后按需追加默认轨迹(A0101);批量/调试路径也走这里,避免只建订单不建轨迹。
+ if (result.Success && request.AppendInitialTrackOnSuccess)
+ {
+ await AppendLifecycleTrackAsync(
+ request.OrderNo,
+ SmallBagTrackLifecycleEvents.CreateTrackCode,
+ authorization,
+ cancellationToken);
+ }
+
+ return result;
}
private async Task AppendLifecycleTrackCoreAsync(
diff --git a/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs b/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs
index ac97499..840fcf9 100644
--- a/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs
+++ b/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs
@@ -81,6 +81,8 @@ public sealed class OrderBatchActionExecutor(
}
request = await smallBagTrackUpsertRequestBuilder.BuildAsync(order, cancellationToken: cancellationToken);
+ // 手动「创建轨迹」无 payload:创单成功后补默认轨迹节点 A0101
+ request.AppendInitialTrackOnSuccess = true;
}
if (string.IsNullOrWhiteSpace(request.Authorization)
@@ -187,13 +189,50 @@ public sealed class OrderBatchActionExecutor(
body,
cancellationToken);
+ ItfxForwardResultEvaluator.IsBusinessSuccess(
+ forward.StatusCode,
+ forward.ResponseBody,
+ out var businessCode,
+ out var businessMessage);
+
if (!forward.Success)
{
+ var reason = ItfxForwardResultEvaluator.BuildFailReason(
+ forward.StatusCode,
+ forward.ResponseBody,
+ businessCode,
+ businessMessage);
return OrderBatchActionExecuteResult.BusinessFail(
- $"下游预报失败: HTTP {forward.StatusCode} {forward.ResponseBody}");
+ reason,
+ resultJson: OrderBatchDownstreamForwardResultBuilder.BuildFailure(
+ forward,
+ message.OrderId,
+ order.ServiceNo,
+ creds.SupplierCode,
+ reason,
+ businessCode,
+ businessMessage));
}
- return OrderBatchActionExecuteResult.Ok();
+ if (ItfxCreateOrderOneDataParser.TryParse(forward.ResponseBody, out var data)
+ && data != null
+ && ItfxCreateOrderOneDataParser.ApplyToOrder(order, data))
+ {
+ await orderRepository.UpdateAsync(order, cancellationToken);
+ }
+
+ return OrderBatchActionExecuteResult.Ok(
+ serviceNo: order.ServiceNo,
+ trackNo: order.TrackNo,
+ resultJson: OrderBatchDownstreamForwardResultBuilder.BuildSuccess(
+ forward,
+ message.OrderId,
+ order.ServiceNo,
+ creds.SupplierCode,
+ businessCode,
+ businessMessage,
+ order.DownstreamServiceNo,
+ order.TrackNo));
}
finally
{
diff --git a/src/Oms.Application/Services/Orders/Batch/OrderBatchDownstreamForwardResultBuilder.cs b/src/Oms.Application/Services/Orders/Batch/OrderBatchDownstreamForwardResultBuilder.cs
new file mode 100644
index 0000000..e2e1bb5
--- /dev/null
+++ b/src/Oms.Application/Services/Orders/Batch/OrderBatchDownstreamForwardResultBuilder.cs
@@ -0,0 +1,87 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Oms.Application.Integrations.Itfx;
+using Oms.Domain.OrdersAggregate.Consts;
+
+namespace Oms.Application.Services.Orders.Batch;
+
+internal static class OrderBatchDownstreamForwardResultBuilder
+{
+ public static string BuildSuccess(
+ ItfxForwardResult forward,
+ int orderId,
+ string? serviceNo,
+ string supplierCode,
+ string? businessCode = null,
+ string? businessMessage = null,
+ string? downstreamServiceNo = null,
+ string? trackNo = null) =>
+ JsonConvert.SerializeObject(new
+ {
+ success = true,
+ action = OrderBatchActionCodes.DownstreamForwardCreateOne,
+ message = "下游预报成功",
+ orderId = orderId > 0 ? orderId : (int?)null,
+ serviceNo,
+ downstreamServiceNo,
+ trackNo,
+ downstreamForward = MapForward(forward, supplierCode, businessCode, businessMessage, downstreamServiceNo, trackNo),
+ });
+
+ public static string BuildFailure(
+ ItfxForwardResult forward,
+ int orderId,
+ string? serviceNo,
+ string supplierCode,
+ string reason,
+ string? businessCode = null,
+ string? businessMessage = null) =>
+ JsonConvert.SerializeObject(new
+ {
+ success = false,
+ action = OrderBatchActionCodes.DownstreamForwardCreateOne,
+ message = reason,
+ orderId = orderId > 0 ? orderId : (int?)null,
+ serviceNo,
+ downstreamForward = MapForward(forward, supplierCode, businessCode, businessMessage),
+ });
+
+ private static object MapForward(
+ ItfxForwardResult forward,
+ string supplierCode,
+ string? businessCode,
+ string? businessMessage,
+ string? downstreamServiceNo = null,
+ string? trackNo = null) =>
+ new
+ {
+ supplierCode,
+ requestUrl = forward.Url,
+ requestBody = TryParseJson(forward.RequestJson),
+ requestBodyRaw = TryParseJson(forward.RequestJson) == null ? forward.RequestJson : null,
+ responseStatusCode = forward.StatusCode,
+ responseBody = TryParseJson(forward.ResponseBody),
+ responseBodyRaw = TryParseJson(forward.ResponseBody) == null ? forward.ResponseBody : null,
+ businessCode,
+ businessMessage,
+ downstreamServiceNo,
+ trackNo,
+ };
+
+ private static object? TryParseJson(string? json)
+ {
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ return null;
+ }
+
+ try
+ {
+ return JToken.Parse(json);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/Oms.Application/Services/Orders/OrderService.DownstreamForward.cs b/src/Oms.Application/Services/Orders/OrderService.DownstreamForward.cs
index 28194bb..1f0f3a4 100644
--- a/src/Oms.Application/Services/Orders/OrderService.DownstreamForward.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.DownstreamForward.cs
@@ -51,6 +51,14 @@ public partial class OrderService
creds.AppSecret,
body);
+ if (forward.Success
+ && ItfxCreateOrderOneDataParser.TryParse(forward.ResponseBody, out var data)
+ && data != null
+ && ItfxCreateOrderOneDataParser.ApplyToOrder(order, data))
+ {
+ await OrderRepository.UpdateAsync(order);
+ }
+
return new OrderDownstreamForwardResult
{
Success = forward.Success,
@@ -58,7 +66,9 @@ public partial class OrderService
Url = forward.Url,
RequestJson = forward.RequestJson,
StatusCode = forward.StatusCode,
- ResponseBody = forward.ResponseBody
+ ResponseBody = forward.ResponseBody,
+ DownstreamServiceNo = order.DownstreamServiceNo,
+ TrackNo = order.TrackNo,
};
}
diff --git a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
index 8105ddd..f40183d 100644
--- a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
@@ -114,6 +114,7 @@ public partial class OrderService
Id = order.Id,
ClientNo = order.ClientNo,
TrackNo = order.TrackNo,
+ DownstreamServiceNo = order.DownstreamServiceNo,
ServiceNo = order.ServiceNo,
ProductCode = order.ProductCode,
ProductName = !string.IsNullOrWhiteSpace(order.ProductCode)
@@ -228,6 +229,7 @@ public partial class OrderService
x.Id,
x.ClientNo,
x.TrackNo,
+ x.DownstreamServiceNo,
x.ServiceNo,
x.ProductCode,
x.ProductName,
diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
index 734a773..c12024a 100644
--- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
@@ -1828,6 +1828,19 @@ public partial class OrderService
}
var request = await BuildSmallBagTrackUpsertRequestAsync(order);
+ request.AppendInitialTrackOnSuccess = true;
+ var authorization = LazyServiceProvider
+ .GetRequiredService()
+ .HttpContext?
+ .Request
+ .Headers
+ .Authorization
+ .ToString();
+ if (!string.IsNullOrWhiteSpace(authorization))
+ {
+ request.Authorization = authorization;
+ }
+
return await SmallBagTrackOrderClient.UpsertDebugAsync(request);
}
diff --git a/src/Oms.Domain/OrdersAggregate/Entities/Order.cs b/src/Oms.Domain/OrdersAggregate/Entities/Order.cs
index f0f7261..aa2d386 100644
--- a/src/Oms.Domain/OrdersAggregate/Entities/Order.cs
+++ b/src/Oms.Domain/OrdersAggregate/Entities/Order.cs
@@ -14,6 +14,12 @@ public class Order : OmsAuditedAggregateRoot
public string? TrackNo { get; set; }
+ ///
+ /// 服务商单号(下游 createOrderOne 返回 Data.serviceNo)。
+ ///
+ [SugarColumn(Length = 64, IsNullable = true)]
+ public string? DownstreamServiceNo { get; set; }
+
public string? ProductCode { get; set; }
public string? ChannelCode { get; set; }
diff --git a/src/Oms.SqlSugarCore/Orders/OrderRepository.cs b/src/Oms.SqlSugarCore/Orders/OrderRepository.cs
index 91074e7..862c0bb 100644
--- a/src/Oms.SqlSugarCore/Orders/OrderRepository.cs
+++ b/src/Oms.SqlSugarCore/Orders/OrderRepository.cs
@@ -58,7 +58,8 @@ public class OrderRepository : OmsSqlSugarRepository, IOrderRepository, I
query = query.Where(x =>
x.ServiceNo.Contains(keyword)
|| (x.ClientNo ?? string.Empty).Contains(keyword)
- || (x.TrackNo ?? string.Empty).Contains(keyword));
+ || (x.TrackNo ?? string.Empty).Contains(keyword)
+ || (x.DownstreamServiceNo ?? string.Empty).Contains(keyword));
}
if (filter.CreateTimeStart.HasValue)
@@ -137,6 +138,12 @@ public class OrderRepository : OmsSqlSugarRepository, IOrderRepository, I
return order;
}
+ order = await FirstOrDefaultAsync(x => x.DownstreamServiceNo == normalized, cancellationToken);
+ if (order != null)
+ {
+ return order;
+ }
+
return await FirstOrDefaultAsync(x => x.ClientNo == normalized, cancellationToken);
}
diff --git a/src/Oms.Tests/ItfxCreateOrderOneDataParserTests.cs b/src/Oms.Tests/ItfxCreateOrderOneDataParserTests.cs
new file mode 100644
index 0000000..3811028
--- /dev/null
+++ b/src/Oms.Tests/ItfxCreateOrderOneDataParserTests.cs
@@ -0,0 +1,69 @@
+using Oms.Application.Integrations.Itfx;
+using Oms.Domain.OrdersAggregate.Entities;
+using Xunit;
+
+namespace Oms.Tests;
+
+public class ItfxCreateOrderOneDataParserTests
+{
+ [Fact]
+ public void TryParse_ReadsPascalCaseData()
+ {
+ const string body = """
+ {
+ "Code": "200",
+ "Msg": "成功",
+ "Data": {
+ "trackNo": null,
+ "serviceNo": "1ZK6B4420326915547",
+ "clientNo": "TEST25091705",
+ "labelUrl": ""
+ }
+ }
+ """;
+
+ Assert.True(ItfxCreateOrderOneDataParser.TryParse(body, out var data));
+ Assert.NotNull(data);
+ Assert.Equal("1ZK6B4420326915547", data!.ServiceNo);
+ Assert.True(string.IsNullOrWhiteSpace(data.TrackNo));
+ Assert.Equal("TEST25091705", data.ClientNo);
+ }
+
+ [Fact]
+ public void ApplyToOrder_WritesDownstreamServiceNoAndTrackNo()
+ {
+ var order = new Order
+ {
+ ServiceNo = "OMS001",
+ TrackNo = null,
+ DownstreamServiceNo = null,
+ };
+ var data = new ItfxCreateOrderOneDataParser.CreateOrderOneData
+ {
+ ServiceNo = "1ZK6B4420326915547",
+ TrackNo = "TRACK123",
+ LabelUrl = "https://label.example/a.pdf",
+ };
+
+ Assert.True(ItfxCreateOrderOneDataParser.ApplyToOrder(order, data));
+ Assert.Equal("OMS001", order.ServiceNo);
+ Assert.Equal("1ZK6B4420326915547", order.DownstreamServiceNo);
+ Assert.Equal("TRACK123", order.TrackNo);
+ Assert.Equal("https://label.example/a.pdf", order.LabelUrl);
+ }
+
+ [Fact]
+ public void ApplyToOrder_DoesNotClearExistingTrackNoWhenNull()
+ {
+ var order = new Order { TrackNo = "OLD", DownstreamServiceNo = null };
+ var data = new ItfxCreateOrderOneDataParser.CreateOrderOneData
+ {
+ ServiceNo = "NEW-SVC",
+ TrackNo = null,
+ };
+
+ Assert.True(ItfxCreateOrderOneDataParser.ApplyToOrder(order, data));
+ Assert.Equal("NEW-SVC", order.DownstreamServiceNo);
+ Assert.Equal("OLD", order.TrackNo);
+ }
+}
diff --git a/src/Oms.Tests/ItfxForwardResultEvaluatorTests.cs b/src/Oms.Tests/ItfxForwardResultEvaluatorTests.cs
new file mode 100644
index 0000000..2d6e635
--- /dev/null
+++ b/src/Oms.Tests/ItfxForwardResultEvaluatorTests.cs
@@ -0,0 +1,25 @@
+using Oms.Application.Integrations.Itfx;
+using Xunit;
+
+namespace Oms.Tests;
+
+public class ItfxForwardResultEvaluatorTests
+{
+ [Theory]
+ [InlineData(200, """{"code":"200","msg":"成功"}""", true)]
+ [InlineData(200, """{"code":"0","msg":"ok"}""", true)]
+ [InlineData(200, """{"code":"400","msg":"未通过状态码验证。"}""", false)]
+ [InlineData(200, """{"Code":400,"Msg":"服务商报错"}""", false)]
+ [InlineData(500, """{"code":"200","msg":"成功"}""", false)] // HTTP 非 2xx 仍失败
+ [InlineData(500, "plain error", false)]
+ [InlineData(200, "", true)]
+ public void IsBusinessSuccess_UsesBodyCodeOverHttpStatus(int httpStatus, string body, bool expected)
+ {
+ var ok = ItfxForwardResultEvaluator.IsBusinessSuccess(httpStatus, body, out var code, out var msg);
+ Assert.Equal(expected, ok);
+ if (body.Contains("400") || body.Contains("未通过") || body.Contains("服务商"))
+ {
+ Assert.False(string.IsNullOrWhiteSpace(code) && string.IsNullOrWhiteSpace(msg));
+ }
+ }
+}