AdoS0ApiErrors.cs 11 KB

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