admin/src/SimpleAdmin.Api/ExceptionHandling/OmsException2ResponseConverter.cs
zzh 41f4c58e99 Improve draft predeclare billing and structured create-fail payload.
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>
2026-07-28 19:30:27 +08:00

142 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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