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