Fix downstream forecast success detection and persist provider service no.
Treat itfx body code/msg as business result, store full request/response, and write Data.serviceNo/trackNo back to the order. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
fffecb5964
commit
6f436f795f
1
db.sql
1
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',
|
||||
|
||||
5
sql/20260728_order_downstream_service_no.sql
Normal file
5
sql/20260728_order_downstream_service_no.sql
Normal file
@ -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`;
|
||||
@ -32,4 +32,14 @@ public class OrderDownstreamForwardResult
|
||||
public int? StatusCode { get; set; }
|
||||
|
||||
public string ResponseBody { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 回写后的服务商单号(Data.serviceNo)。
|
||||
/// </summary>
|
||||
public string? DownstreamServiceNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 回写后的跟踪号(Data.trackNo)。
|
||||
/// </summary>
|
||||
public string? TrackNo { get; set; }
|
||||
}
|
||||
|
||||
@ -20,6 +20,11 @@ public class OrderOutput
|
||||
/// </summary>
|
||||
public string? TrackNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务商单号(下游 createOrderOne Data.serviceNo)。
|
||||
/// </summary>
|
||||
public string? DownstreamServiceNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务单号。
|
||||
/// </summary>
|
||||
|
||||
@ -25,6 +25,11 @@ public class OrderPaginatedOutput
|
||||
/// </summary>
|
||||
public string? TrackNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务商单号(下游 createOrderOne Data.serviceNo)。
|
||||
/// </summary>
|
||||
public string? DownstreamServiceNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品编码。
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,145 @@
|
||||
using System.Text.Json;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Integrations.Itfx;
|
||||
|
||||
/// <summary>
|
||||
/// 解析 itfx createOrderOne 成功响应中的 Data 字段,并回写订单。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将下游返回写入订单:服务商单号 ← Data.serviceNo,跟踪号 ← Data.trackNo。
|
||||
/// 空值不覆盖已有字段。
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Oms.Application.Integrations.Itfx;
|
||||
|
||||
/// <summary>
|
||||
/// itfx ForwardWithSubscribe 业务成败判定。
|
||||
/// 注意:业务失败时常仍返回 HTTP 200,body.code=400。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1828,6 +1828,19 @@ public partial class OrderService
|
||||
}
|
||||
|
||||
var request = await BuildSmallBagTrackUpsertRequestAsync(order);
|
||||
request.AppendInitialTrackOnSuccess = true;
|
||||
var authorization = LazyServiceProvider
|
||||
.GetRequiredService<IHttpContextAccessor>()
|
||||
.HttpContext?
|
||||
.Request
|
||||
.Headers
|
||||
.Authorization
|
||||
.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(authorization))
|
||||
{
|
||||
request.Authorization = authorization;
|
||||
}
|
||||
|
||||
return await SmallBagTrackOrderClient.UpsertDebugAsync(request);
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,12 @@ public class Order : OmsAuditedAggregateRoot
|
||||
|
||||
public string? TrackNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务商单号(下游 createOrderOne 返回 Data.serviceNo)。
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 64, IsNullable = true)]
|
||||
public string? DownstreamServiceNo { get; set; }
|
||||
|
||||
public string? ProductCode { get; set; }
|
||||
|
||||
public string? ChannelCode { get; set; }
|
||||
|
||||
@ -58,7 +58,8 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, 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<Order>, 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);
|
||||
}
|
||||
|
||||
|
||||
69
src/Oms.Tests/ItfxCreateOrderOneDataParserTests.cs
Normal file
69
src/Oms.Tests/ItfxCreateOrderOneDataParserTests.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
25
src/Oms.Tests/ItfxForwardResultEvaluatorTests.cs
Normal file
25
src/Oms.Tests/ItfxForwardResultEvaluatorTests.cs
Normal file
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user