AdoS0ApiErrors.cs 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. using System.Reflection;
  2. using Microsoft.AspNetCore.Http;
  3. using Microsoft.AspNetCore.Mvc.Filters;
  4. using Microsoft.Extensions.Logging;
  5. namespace Admin.NET.Plugin.AiDOP.Infrastructure;
  6. public sealed class AdoS0ApiErrorResponse
  7. {
  8. public string Code { get; init; } = string.Empty;
  9. public string Message { get; init; } = string.Empty;
  10. }
  11. public static class AdoS0ApiErrors
  12. {
  13. private static readonly (string Prefix, string Code, string Message)[] DuplicateCodeMappings =
  14. [
  15. ("/api/s0/sales/customers", AdoS0ErrorCodes.CustomerCodeExists, "客户编码已存在"),
  16. ("/api/s0/sales/materials", AdoS0ErrorCodes.MaterialCodeExists, "物料编码已存在"),
  17. ("/api/s0/sales/order-priority-rules", AdoS0ErrorCodes.OrderPriorityRuleCodeExists, "订单优先规则编码已存在"),
  18. ("/api/s0/manufacturing/boms", AdoS0ErrorCodes.DuplicateCode, "BOM 编码已存在"),
  19. ("/api/s0/manufacturing/standard-operations", AdoS0ErrorCodes.DuplicateCode, "标准工序编码已存在"),
  20. ("/api/s0/manufacturing/production-lines", AdoS0ErrorCodes.DuplicateCode, "产线编码已存在"),
  21. ("/api/s0/manufacturing/routings", AdoS0ErrorCodes.DuplicateCode, "工艺路线编码已存在"),
  22. ("/api/s0/manufacturing/work-order-controls", AdoS0ErrorCodes.DuplicateCode, "工单控制参数编码已存在"),
  23. ("/api/s0/manufacturing/person-skills", AdoS0ErrorCodes.DuplicateCode, "人员技能编码已存在"),
  24. ("/api/s0/manufacturing/line-posts", AdoS0ErrorCodes.DuplicateCode, "线体岗位编码已存在"),
  25. ("/api/s0/manufacturing/work-centers", AdoS0ErrorCodes.DuplicateCode, "工作中心编码已存在"),
  26. ("/api/s0/manufacturing/preprocess-elements", AdoS0ErrorCodes.DuplicateCode, "前处理要素编码已存在"),
  27. ("/api/s0/manufacturing/sop-file-types", AdoS0ErrorCodes.DuplicateCode, "SOP 文件类型编码已存在"),
  28. ("/api/s0/manufacturing/sop-documents", AdoS0ErrorCodes.DuplicateCode, "SOP 文档编码已存在")
  29. ];
  30. public static bool IsS0Request(PathString path) =>
  31. path.Value?.StartsWith("/api/s0/", StringComparison.OrdinalIgnoreCase) == true;
  32. public static ObjectResult InvalidRequest(string message) =>
  33. Create(StatusCodes.Status400BadRequest, AdoS0ErrorCodes.InvalidRequest, string.IsNullOrWhiteSpace(message) ? "请求参数非法" : message);
  34. public static ObjectResult InvalidReference(string code, string message) =>
  35. Create(StatusCodes.Status400BadRequest, code, message);
  36. public static ObjectResult Conflict(string code, string message) =>
  37. Create(StatusCodes.Status409Conflict, code, message);
  38. public static ObjectResult NotFound(string message = "记录不存在") =>
  39. Create(StatusCodes.Status404NotFound, AdoS0ErrorCodes.RecordNotFound, message);
  40. public static ObjectResult InternalServerError(string message = "系统繁忙,请稍后再试") =>
  41. Create(StatusCodes.Status500InternalServerError, AdoS0ErrorCodes.InternalServerError, message);
  42. public static IActionResult WrapResult(IActionResult result)
  43. {
  44. if (result is ObjectResult { Value: AdoS0ApiErrorResponse })
  45. return result;
  46. return result switch
  47. {
  48. NotFoundResult => NotFound(),
  49. NotFoundObjectResult notFound => NotFound(ExtractMessage(notFound.Value) ?? "记录不存在"),
  50. BadRequestObjectResult badRequest => WrapBadRequest(badRequest),
  51. ObjectResult { StatusCode: StatusCodes.Status400BadRequest } objectResult => WrapBadRequest(objectResult),
  52. _ => result
  53. };
  54. }
  55. public static bool TryMapUnhandledException(PathString path, Exception exception, out ObjectResult? result)
  56. {
  57. result = null;
  58. if (!IsS0Request(path))
  59. return false;
  60. if (ContainsDuplicateKey(exception))
  61. {
  62. var mapping = ResolveDuplicateCode(path);
  63. result = Conflict(mapping.Code, mapping.Message);
  64. return true;
  65. }
  66. result = InternalServerError();
  67. return true;
  68. }
  69. private static ObjectResult WrapBadRequest(ObjectResult result)
  70. {
  71. var message = ExtractMessage(result.Value) ?? "请求参数非法";
  72. var mapping = ResolveBadRequest(message);
  73. return Create(StatusCodes.Status400BadRequest, mapping.Code, mapping.Message);
  74. }
  75. private static (string Code, string Message) ResolveBadRequest(string message)
  76. {
  77. var normalized = message.Trim();
  78. return normalized switch
  79. {
  80. "BOM 至少包含一行子项" => (AdoS0ErrorCodes.BomItemRequired, normalized),
  81. "同一 BOM 下子项物料不能重复" => (AdoS0ErrorCodes.BomItemDuplicate, normalized),
  82. "父项物料不能同时作为本子项出现" => (AdoS0ErrorCodes.BomParentConflict, normalized),
  83. "数量分母不能为 0" => (AdoS0ErrorCodes.BomQtyDenominatorZero, normalized),
  84. "工艺路线至少包含一道工序" => (AdoS0ErrorCodes.RoutingOperationRequired, normalized),
  85. "存在无效的标准工序引用" => (AdoS0ErrorCodes.StandardOperationReferenceInvalid, normalized),
  86. "物料主数据引用无效" => (AdoS0ErrorCodes.MaterialReferenceInvalid, normalized),
  87. "前处理要素引用无效" => (AdoS0ErrorCodes.PreprocessElementReferenceInvalid, normalized),
  88. "生产要素参数引用无效" => (AdoS0ErrorCodes.ElementParamReferenceInvalid, normalized),
  89. "存在无效的生产要素参数引用" => (AdoS0ErrorCodes.ElementParamReferenceInvalid, normalized),
  90. "存在无效的人员技能主数据引用" => (AdoS0ErrorCodes.PersonSkillReferenceInvalid, normalized),
  91. _ when normalized.Contains("引用无效", StringComparison.Ordinal) =>
  92. (AdoS0ErrorCodes.InvalidReference, normalized),
  93. _ => (AdoS0ErrorCodes.InvalidRequest, normalized)
  94. };
  95. }
  96. private static (string Code, string Message) ResolveDuplicateCode(PathString path)
  97. {
  98. var requestPath = path.Value ?? string.Empty;
  99. foreach (var mapping in DuplicateCodeMappings)
  100. {
  101. if (requestPath.StartsWith(mapping.Prefix, StringComparison.OrdinalIgnoreCase))
  102. return (mapping.Code, mapping.Message);
  103. }
  104. return (AdoS0ErrorCodes.DuplicateCode, "编码已存在");
  105. }
  106. private static string? ExtractMessage(object? value)
  107. {
  108. if (value == null) return null;
  109. if (value is string text) return text;
  110. if (value is ValidationProblemDetails validationProblem)
  111. return FlattenErrors(validationProblem.Errors);
  112. if (value is ProblemDetails problemDetails)
  113. return problemDetails.Detail ?? problemDetails.Title;
  114. if (value is IDictionary<string, string[]> stringArrayDictionary)
  115. return FlattenErrors(stringArrayDictionary);
  116. if (value is IDictionary<string, object?> objectDictionary)
  117. {
  118. foreach (var item in objectDictionary)
  119. {
  120. if (item.Key.Equals("message", StringComparison.OrdinalIgnoreCase))
  121. return item.Value?.ToString();
  122. }
  123. }
  124. var messageProperty = value.GetType().GetProperty("message", BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
  125. return messageProperty?.GetValue(value)?.ToString();
  126. }
  127. private static string FlattenErrors(IDictionary<string, string[]> errors)
  128. {
  129. var parts = errors
  130. .SelectMany(pair => pair.Value.Select(message => string.IsNullOrWhiteSpace(pair.Key) ? message : $"{pair.Key}: {message}"))
  131. .Where(message => !string.IsNullOrWhiteSpace(message))
  132. .ToList();
  133. return parts.Count == 0 ? "请求参数非法" : string.Join(";", parts);
  134. }
  135. private static bool ContainsDuplicateKey(Exception exception)
  136. {
  137. for (var current = exception; current != null; current = current.InnerException)
  138. {
  139. var message = current.Message;
  140. if (string.IsNullOrWhiteSpace(message))
  141. continue;
  142. if (message.Contains("Duplicate entry", StringComparison.OrdinalIgnoreCase) ||
  143. message.Contains("UNIQUE constraint failed", StringComparison.OrdinalIgnoreCase))
  144. {
  145. return true;
  146. }
  147. }
  148. return false;
  149. }
  150. private static ObjectResult Create(int statusCode, string code, string message)
  151. {
  152. return new ObjectResult(new AdoS0ApiErrorResponse
  153. {
  154. Code = code,
  155. Message = message
  156. })
  157. {
  158. StatusCode = statusCode
  159. };
  160. }
  161. }
  162. public sealed class AdoS0ResultFilter : IAsyncAlwaysRunResultFilter
  163. {
  164. public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
  165. {
  166. if (AdoS0ApiErrors.IsS0Request(context.HttpContext.Request.Path))
  167. context.Result = AdoS0ApiErrors.WrapResult(context.Result);
  168. await next();
  169. }
  170. }
  171. public sealed class AdoS0ExceptionFilter : IAsyncExceptionFilter
  172. {
  173. private readonly ILogger<AdoS0ExceptionFilter> _logger;
  174. public AdoS0ExceptionFilter(ILogger<AdoS0ExceptionFilter> logger)
  175. {
  176. _logger = logger;
  177. }
  178. public Task OnExceptionAsync(ExceptionContext context)
  179. {
  180. if (!AdoS0ApiErrors.TryMapUnhandledException(context.HttpContext.Request.Path, context.Exception, out var result) || result == null)
  181. return Task.CompletedTask;
  182. if (result.StatusCode == StatusCodes.Status409Conflict)
  183. _logger.LogWarning(context.Exception, "S0 duplicate conflict: {Path}", context.HttpContext.Request.Path);
  184. else
  185. _logger.LogError(context.Exception, "S0 unhandled exception: {Path}", context.HttpContext.Request.Path);
  186. context.Result = result;
  187. context.ExceptionHandled = true;
  188. return Task.CompletedTask;
  189. }
  190. }