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 '运单号',
|
`ServiceNo` VARCHAR(64) NOT NULL COMMENT '运单号',
|
||||||
`ClientNo` VARCHAR(64) NULL COMMENT '客户单号',
|
`ClientNo` VARCHAR(64) NULL COMMENT '客户单号',
|
||||||
`TrackNo` VARCHAR(64) NULL COMMENT '跟踪号',
|
`TrackNo` VARCHAR(64) NULL COMMENT '跟踪号',
|
||||||
|
`DownstreamServiceNo` VARCHAR(64) NULL COMMENT '服务商单号(下游 createOrderOne Data.serviceNo)',
|
||||||
`ProductCode` VARCHAR(128) NULL COMMENT '产品编码',
|
`ProductCode` VARCHAR(128) NULL COMMENT '产品编码',
|
||||||
`ChannelCode` VARCHAR(128) NULL COMMENT '渠道编码',
|
`ChannelCode` VARCHAR(128) NULL COMMENT '渠道编码',
|
||||||
`DownstreamSupplierCode` VARCHAR(64) NULL COMMENT '下游 API 服务商编码 supplierCode',
|
`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 int? StatusCode { get; set; }
|
||||||
|
|
||||||
public string ResponseBody { get; set; } = string.Empty;
|
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>
|
/// </summary>
|
||||||
public string? TrackNo { get; set; }
|
public string? TrackNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 服务商单号(下游 createOrderOne Data.serviceNo)。
|
||||||
|
/// </summary>
|
||||||
|
public string? DownstreamServiceNo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 服务单号。
|
/// 服务单号。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -25,6 +25,11 @@ public class OrderPaginatedOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? TrackNo { get; set; }
|
public string? TrackNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 服务商单号(下游 createOrderOne Data.serviceNo)。
|
||||||
|
/// </summary>
|
||||||
|
public string? DownstreamServiceNo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 产品编码。
|
/// 产品编码。
|
||||||
/// </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);
|
using var response = await client.SendAsync(request, cancellationToken);
|
||||||
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var statusCode = (int)response.StatusCode;
|
||||||
|
var success = ItfxForwardResultEvaluator.IsBusinessSuccess(
|
||||||
|
statusCode,
|
||||||
|
responseBody,
|
||||||
|
out _,
|
||||||
|
out _);
|
||||||
|
|
||||||
return new ItfxForwardResult
|
return new ItfxForwardResult
|
||||||
{
|
{
|
||||||
Success = response.IsSuccessStatusCode,
|
Success = success,
|
||||||
Url = url,
|
Url = url,
|
||||||
RequestJson = requestJson,
|
RequestJson = requestJson,
|
||||||
StatusCode = (int)response.StatusCode,
|
StatusCode = statusCode,
|
||||||
ResponseBody = responseBody
|
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);
|
logger.LogInformation("UpsertSmallBag succeeded for order {OrderNo}", request.OrderNo);
|
||||||
|
|
||||||
if (request.AppendInitialTrackOnSuccess)
|
|
||||||
{
|
|
||||||
await AppendLifecycleTrackAsync(
|
|
||||||
request.OrderNo,
|
|
||||||
SmallBagTrackLifecycleEvents.CreateTrackCode,
|
|
||||||
request.Authorization,
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task AppendLifecycleTrackAsync(
|
public Task AppendLifecycleTrackAsync(
|
||||||
@ -105,7 +96,7 @@ internal sealed class SmallBagTrackOrderClient(
|
|||||||
body,
|
body,
|
||||||
JsonOptions);
|
JsonOptions);
|
||||||
|
|
||||||
return new SmallBagTrackUpsertResult
|
var result = new SmallBagTrackUpsertResult
|
||||||
{
|
{
|
||||||
Skipped = false,
|
Skipped = false,
|
||||||
Success = response.Response.IsSuccessStatusCode,
|
Success = response.Response.IsSuccessStatusCode,
|
||||||
@ -115,6 +106,18 @@ internal sealed class SmallBagTrackOrderClient(
|
|||||||
StatusCode = (int)response.Response.StatusCode,
|
StatusCode = (int)response.Response.StatusCode,
|
||||||
ResponseBody = response.ResponseBody
|
ResponseBody = response.ResponseBody
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 创单成功后按需追加默认轨迹(A0101);批量/调试路径也走这里,避免只建订单不建轨迹。
|
||||||
|
if (result.Success && request.AppendInitialTrackOnSuccess)
|
||||||
|
{
|
||||||
|
await AppendLifecycleTrackAsync(
|
||||||
|
request.OrderNo,
|
||||||
|
SmallBagTrackLifecycleEvents.CreateTrackCode,
|
||||||
|
authorization,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AppendLifecycleTrackCoreAsync(
|
private async Task AppendLifecycleTrackCoreAsync(
|
||||||
|
|||||||
@ -81,6 +81,8 @@ public sealed class OrderBatchActionExecutor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
request = await smallBagTrackUpsertRequestBuilder.BuildAsync(order, cancellationToken: cancellationToken);
|
request = await smallBagTrackUpsertRequestBuilder.BuildAsync(order, cancellationToken: cancellationToken);
|
||||||
|
// 手动「创建轨迹」无 payload:创单成功后补默认轨迹节点 A0101
|
||||||
|
request.AppendInitialTrackOnSuccess = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Authorization)
|
if (string.IsNullOrWhiteSpace(request.Authorization)
|
||||||
@ -187,13 +189,50 @@ public sealed class OrderBatchActionExecutor(
|
|||||||
body,
|
body,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
|
ItfxForwardResultEvaluator.IsBusinessSuccess(
|
||||||
|
forward.StatusCode,
|
||||||
|
forward.ResponseBody,
|
||||||
|
out var businessCode,
|
||||||
|
out var businessMessage);
|
||||||
|
|
||||||
if (!forward.Success)
|
if (!forward.Success)
|
||||||
{
|
{
|
||||||
|
var reason = ItfxForwardResultEvaluator.BuildFailReason(
|
||||||
|
forward.StatusCode,
|
||||||
|
forward.ResponseBody,
|
||||||
|
businessCode,
|
||||||
|
businessMessage);
|
||||||
return OrderBatchActionExecuteResult.BusinessFail(
|
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
|
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,
|
creds.AppSecret,
|
||||||
body);
|
body);
|
||||||
|
|
||||||
|
if (forward.Success
|
||||||
|
&& ItfxCreateOrderOneDataParser.TryParse(forward.ResponseBody, out var data)
|
||||||
|
&& data != null
|
||||||
|
&& ItfxCreateOrderOneDataParser.ApplyToOrder(order, data))
|
||||||
|
{
|
||||||
|
await OrderRepository.UpdateAsync(order);
|
||||||
|
}
|
||||||
|
|
||||||
return new OrderDownstreamForwardResult
|
return new OrderDownstreamForwardResult
|
||||||
{
|
{
|
||||||
Success = forward.Success,
|
Success = forward.Success,
|
||||||
@ -58,7 +66,9 @@ public partial class OrderService
|
|||||||
Url = forward.Url,
|
Url = forward.Url,
|
||||||
RequestJson = forward.RequestJson,
|
RequestJson = forward.RequestJson,
|
||||||
StatusCode = forward.StatusCode,
|
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,
|
Id = order.Id,
|
||||||
ClientNo = order.ClientNo,
|
ClientNo = order.ClientNo,
|
||||||
TrackNo = order.TrackNo,
|
TrackNo = order.TrackNo,
|
||||||
|
DownstreamServiceNo = order.DownstreamServiceNo,
|
||||||
ServiceNo = order.ServiceNo,
|
ServiceNo = order.ServiceNo,
|
||||||
ProductCode = order.ProductCode,
|
ProductCode = order.ProductCode,
|
||||||
ProductName = !string.IsNullOrWhiteSpace(order.ProductCode)
|
ProductName = !string.IsNullOrWhiteSpace(order.ProductCode)
|
||||||
@ -228,6 +229,7 @@ public partial class OrderService
|
|||||||
x.Id,
|
x.Id,
|
||||||
x.ClientNo,
|
x.ClientNo,
|
||||||
x.TrackNo,
|
x.TrackNo,
|
||||||
|
x.DownstreamServiceNo,
|
||||||
x.ServiceNo,
|
x.ServiceNo,
|
||||||
x.ProductCode,
|
x.ProductCode,
|
||||||
x.ProductName,
|
x.ProductName,
|
||||||
|
|||||||
@ -1828,6 +1828,19 @@ public partial class OrderService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var request = await BuildSmallBagTrackUpsertRequestAsync(order);
|
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);
|
return await SmallBagTrackOrderClient.UpsertDebugAsync(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,12 @@ public class Order : OmsAuditedAggregateRoot
|
|||||||
|
|
||||||
public string? TrackNo { get; set; }
|
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? ProductCode { get; set; }
|
||||||
|
|
||||||
public string? ChannelCode { get; set; }
|
public string? ChannelCode { get; set; }
|
||||||
|
|||||||
@ -58,7 +58,8 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
|||||||
query = query.Where(x =>
|
query = query.Where(x =>
|
||||||
x.ServiceNo.Contains(keyword)
|
x.ServiceNo.Contains(keyword)
|
||||||
|| (x.ClientNo ?? string.Empty).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)
|
if (filter.CreateTimeStart.HasValue)
|
||||||
@ -137,6 +138,12 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
|||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
order = await FirstOrDefaultAsync(x => x.DownstreamServiceNo == normalized, cancellationToken);
|
||||||
|
if (order != null)
|
||||||
|
{
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
return await FirstOrDefaultAsync(x => x.ClientNo == normalized, cancellationToken);
|
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