Re-run forecast billing on draft pre-declare, and expose draftSaved/orderId/serviceNo via ErrorCode/Data/details for clearer place-order failure UX. Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
5.1 KiB
C#
142 lines
5.1 KiB
C#
using System.Collections;
|
||
using Fake.Application;
|
||
using Fake.Application.Dtos;
|
||
using Fake.AspNetCore.ExceptionHandling;
|
||
using Newtonsoft.Json;
|
||
using Newtonsoft.Json.Serialization;
|
||
using Oms.Application.Dtos.Orders;
|
||
|
||
namespace SimpleAdmin.Api.ExceptionHandling;
|
||
|
||
/// <summary>
|
||
/// 在 Fake 默认异常转换之上,把草稿已落库失败的结构化信息写入 Details,并清理 msg 中的机器标记。
|
||
/// </summary>
|
||
public class OmsException2ResponseConverter : IException2ResponseConverter
|
||
{
|
||
private static readonly JsonSerializerSettings JsonSettings = new()
|
||
{
|
||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||
NullValueHandling = NullValueHandling.Ignore,
|
||
};
|
||
|
||
private const string DraftSavedMarkerPrefix = " [[omsDraft:";
|
||
|
||
private readonly DefaultException2ResponseConverter _inner;
|
||
|
||
public OmsException2ResponseConverter(DefaultException2ResponseConverter inner)
|
||
{
|
||
_inner = inner;
|
||
}
|
||
|
||
public ApplicationExceptionResponse Convert(Exception exception, FakeExceptionHandlingOptions options)
|
||
{
|
||
var response = _inner.Convert(exception, options);
|
||
response.Message = StripDraftSavedMarker(response.Message);
|
||
|
||
if (TryBuildDraftFailData(exception, out var failData))
|
||
{
|
||
if (string.IsNullOrWhiteSpace(response.Message))
|
||
{
|
||
response.Message = failData.Reason;
|
||
}
|
||
|
||
response.Details = JsonConvert.SerializeObject(failData, JsonSettings);
|
||
}
|
||
|
||
return response;
|
||
}
|
||
|
||
private static bool TryBuildDraftFailData(Exception exception, out OrderParcelDraftFailData failData)
|
||
{
|
||
failData = new OrderParcelDraftFailData();
|
||
if (exception is not BusinessException business)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (business.Data[OrderParcelDraftFailData.DataKey] is string json
|
||
&& !string.IsNullOrWhiteSpace(json))
|
||
{
|
||
try
|
||
{
|
||
var parsed = JsonConvert.DeserializeObject<OrderParcelDraftFailData>(json);
|
||
if (parsed != null && parsed.DraftSaved && parsed.OrderId > 0)
|
||
{
|
||
failData = parsed;
|
||
return true;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// fall through to key-based parse
|
||
}
|
||
}
|
||
|
||
if (!HasDraftSavedFlag(business.Data))
|
||
{
|
||
// 兼容旧客户端:仅从 message 尾部标记解析
|
||
return TryParseMarker(business.Message, out failData);
|
||
}
|
||
|
||
failData.DraftSaved = true;
|
||
failData.OrderId = ToInt(business.Data["orderId"] ?? business.Data["OrderId"]);
|
||
failData.ServiceNo = ToStringValue(business.Data["serviceNo"] ?? business.Data["ServiceNo"]);
|
||
failData.Reason = StripDraftSavedMarker(business.Message);
|
||
failData.ErrorCode = OrderParcelDraftFailData.ErrorCodeValue;
|
||
return failData.OrderId > 0;
|
||
}
|
||
|
||
private static bool HasDraftSavedFlag(IDictionary data)
|
||
{
|
||
if (data.Contains("draftSaved") && IsTruthy(data["draftSaved"])) return true;
|
||
if (data.Contains("DraftSaved") && IsTruthy(data["DraftSaved"])) return true;
|
||
return false;
|
||
}
|
||
|
||
private static bool TryParseMarker(string? message, out OrderParcelDraftFailData failData)
|
||
{
|
||
failData = new OrderParcelDraftFailData();
|
||
if (string.IsNullOrEmpty(message)) return false;
|
||
var idx = message.IndexOf(DraftSavedMarkerPrefix, StringComparison.Ordinal);
|
||
if (idx < 0) return false;
|
||
var jsonStart = idx + DraftSavedMarkerPrefix.Length;
|
||
var jsonEnd = message.LastIndexOf("]]", StringComparison.Ordinal);
|
||
if (jsonEnd <= jsonStart) return false;
|
||
try
|
||
{
|
||
var parsed = JsonConvert.DeserializeObject<OrderParcelDraftFailData>(message[jsonStart..jsonEnd]);
|
||
if (parsed == null || !parsed.DraftSaved || parsed.OrderId <= 0) return false;
|
||
parsed.Reason = message[..idx].TrimEnd();
|
||
parsed.ErrorCode = OrderParcelDraftFailData.ErrorCodeValue;
|
||
failData = parsed;
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static string StripDraftSavedMarker(string? message)
|
||
{
|
||
if (string.IsNullOrEmpty(message)) return string.Empty;
|
||
var idx = message.IndexOf(DraftSavedMarkerPrefix, StringComparison.Ordinal);
|
||
return idx < 0 ? message : message[..idx].TrimEnd();
|
||
}
|
||
|
||
private static bool IsTruthy(object? value) =>
|
||
value is true
|
||
|| (value is bool b && b)
|
||
|| string.Equals(global::System.Convert.ToString(value), "true", StringComparison.OrdinalIgnoreCase)
|
||
|| string.Equals(global::System.Convert.ToString(value), "1", StringComparison.OrdinalIgnoreCase);
|
||
|
||
private static int ToInt(object? value)
|
||
{
|
||
if (value == null) return 0;
|
||
return int.TryParse(global::System.Convert.ToString(value), out var n) ? n : 0;
|
||
}
|
||
|
||
private static string ToStringValue(object? value) =>
|
||
global::System.Convert.ToString(value)?.Trim() ?? string.Empty;
|
||
}
|