using System.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; namespace Admin.NET.Plugin.AiDOP.Infrastructure; public sealed class AdoS0ApiErrorResponse { public string Code { get; init; } = string.Empty; public string Message { get; init; } = string.Empty; } public static class AdoS0ApiErrors { private static readonly (string Prefix, string Code, string Message)[] DuplicateCodeMappings = [ ("/api/s0/sales/customers", AdoS0ErrorCodes.CustomerCodeExists, "客户编码已存在"), ("/api/s0/sales/materials", AdoS0ErrorCodes.MaterialCodeExists, "物料编码已存在"), ("/api/s0/supply/suppliers", AdoS0ErrorCodes.SupplierCodeExists, "供应商编码已存在"), ("/api/s0/manufacturing/standard-operations", AdoS0ErrorCodes.DuplicateCode, "标准工序名称已存在"), ("/api/s0/manufacturing/production-lines", AdoS0ErrorCodes.DuplicateCode, "生产线编码已存在"), ("/api/s0/manufacturing/routings", AdoS0ErrorCodes.DuplicateCode, "工艺路线编码已存在"), ("/api/s0/manufacturing/work-order-controls", AdoS0ErrorCodes.DuplicateCode, "工单控制参数编码已存在"), ("/api/s0/manufacturing/person-skills", AdoS0ErrorCodes.DuplicateCode, "人员技能编码已存在"), ("/api/s0/manufacturing/line-posts", AdoS0ErrorCodes.DuplicateCode, "线体岗位编码已存在"), ("/api/s0/manufacturing/work-centers", AdoS0ErrorCodes.DuplicateCode, "工作中心编码已存在"), ("/api/s0/manufacturing/preprocess-elements", AdoS0ErrorCodes.DuplicateCode, "前处理要素编码已存在"), ("/api/s0/manufacturing/sop-file-types", AdoS0ErrorCodes.DuplicateCode, "SOP 文件类型编码已存在"), ("/api/s0/manufacturing/sop-documents", AdoS0ErrorCodes.DuplicateCode, "SOP 文档编码已存在"), ("/api/s0/warehouse/departments", AdoS0ErrorCodes.DepartmentCodeExists, "部门编码已存在"), ("/api/s0/warehouse/employees", AdoS0ErrorCodes.EmployeeCodeExists, "员工编码已存在"), ("/api/s0/warehouse/cost-centers", AdoS0ErrorCodes.CostCtrCodeExists, "成本中心编码已存在"), ("/api/s0/warehouse/locations", AdoS0ErrorCodes.LocationCodeExists, "库位编码已存在"), ("/api/s0/warehouse/location-shelves", AdoS0ErrorCodes.LocationShelfCodeExists, "货架编码已存在"), ("/api/s0/warehouse/barcode-rules", AdoS0ErrorCodes.BarcodeRuleCodeExists, "条码规则已存在"), ("/api/s0/warehouse/label-types", AdoS0ErrorCodes.LabelTypeCodeExists, "标签类型编码已存在"), ("/api/s0/warehouse/nbr-types", AdoS0ErrorCodes.NbrTypeCodeExists, "单号类型编码已存在"), ("/api/s0/warehouse/nbr-controls", AdoS0ErrorCodes.NbrControlCodeExists, "单号规则编码已存在"), ("/api/s0/warehouse/item-packs", AdoS0ErrorCodes.ItemPackCodeExists, "物料包装规格已存在"), ("/api/s0/warehouse/emp-work-duties", AdoS0ErrorCodes.DuplicateCode, "物料职责记录已存在"), ("/api/s0/warehouse/task-assignments", AdoS0ErrorCodes.TaskAssignmentCodeExists, "任务指派记录已存在") ]; public static bool IsS0Request(PathString path) => path.Value?.StartsWith("/api/s0/", StringComparison.OrdinalIgnoreCase) == true; public static ObjectResult InvalidRequest(string message) => Create(StatusCodes.Status400BadRequest, AdoS0ErrorCodes.InvalidRequest, string.IsNullOrWhiteSpace(message) ? "请求参数非法" : message); public static ObjectResult InvalidReference(string code, string message) => Create(StatusCodes.Status400BadRequest, code, message); public static ObjectResult Conflict(string code, string message) => Create(StatusCodes.Status409Conflict, code, message); public static ObjectResult NotFound(string message = "记录不存在") => Create(StatusCodes.Status404NotFound, AdoS0ErrorCodes.RecordNotFound, message); public static ObjectResult InternalServerError(string message = "系统繁忙,请稍后再试") => Create(StatusCodes.Status500InternalServerError, AdoS0ErrorCodes.InternalServerError, message); public static IActionResult WrapResult(IActionResult result) { if (result is ObjectResult { Value: AdoS0ApiErrorResponse }) return result; return result switch { NotFoundResult => NotFound(), NotFoundObjectResult notFound => NotFound(ExtractMessage(notFound.Value) ?? "记录不存在"), BadRequestObjectResult badRequest => WrapBadRequest(badRequest), ObjectResult { StatusCode: StatusCodes.Status400BadRequest } objectResult => WrapBadRequest(objectResult), _ => result }; } public static bool TryMapUnhandledException(PathString path, Exception exception, out ObjectResult? result) { result = null; if (!IsS0Request(path)) return false; if (ContainsDuplicateKey(exception)) { var mapping = ResolveDuplicateCode(path); result = Conflict(mapping.Code, mapping.Message); return true; } result = InternalServerError(); return true; } private static ObjectResult WrapBadRequest(ObjectResult result) { var message = ExtractMessage(result.Value) ?? "请求参数非法"; var mapping = ResolveBadRequest(message); return Create(StatusCodes.Status400BadRequest, mapping.Code, mapping.Message); } private static (string Code, string Message) ResolveBadRequest(string message) { var normalized = message.Trim(); return normalized switch { "BOM 至少包含一行子项" => (AdoS0ErrorCodes.BomItemRequired, normalized), "同一 BOM 下子项物料不能重复" => (AdoS0ErrorCodes.BomItemDuplicate, normalized), "父项物料不能同时作为本子项出现" => (AdoS0ErrorCodes.BomParentConflict, normalized), "数量分母不能为 0" => (AdoS0ErrorCodes.BomQtyDenominatorZero, normalized), "工艺路线至少包含一道工序" => (AdoS0ErrorCodes.RoutingOperationRequired, normalized), "存在无效的标准工序引用" => (AdoS0ErrorCodes.StandardOperationReferenceInvalid, normalized), "物料主数据引用无效" => (AdoS0ErrorCodes.MaterialReferenceInvalid, normalized), "前处理要素引用无效" => (AdoS0ErrorCodes.PreprocessElementReferenceInvalid, normalized), "生产要素参数引用无效" => (AdoS0ErrorCodes.ElementParamReferenceInvalid, normalized), "存在无效的生产要素参数引用" => (AdoS0ErrorCodes.ElementParamReferenceInvalid, normalized), "存在无效的人员技能主数据引用" => (AdoS0ErrorCodes.PersonSkillReferenceInvalid, normalized), _ when normalized.Contains("引用无效", StringComparison.Ordinal) => (AdoS0ErrorCodes.InvalidReference, normalized), _ => (AdoS0ErrorCodes.InvalidRequest, normalized) }; } private static (string Code, string Message) ResolveDuplicateCode(PathString path) { var requestPath = path.Value ?? string.Empty; foreach (var mapping in DuplicateCodeMappings) { if (requestPath.StartsWith(mapping.Prefix, StringComparison.OrdinalIgnoreCase)) return (mapping.Code, mapping.Message); } return (AdoS0ErrorCodes.DuplicateCode, "编码已存在"); } private static string? ExtractMessage(object? value) { if (value == null) return null; if (value is string text) return text; if (value is ValidationProblemDetails validationProblem) return FlattenErrors(validationProblem.Errors); if (value is ProblemDetails problemDetails) return problemDetails.Detail ?? problemDetails.Title; if (value is IDictionary stringArrayDictionary) return FlattenErrors(stringArrayDictionary); if (value is IDictionary objectDictionary) { foreach (var item in objectDictionary) { if (item.Key.Equals("message", StringComparison.OrdinalIgnoreCase)) return item.Value?.ToString(); } } var messageProperty = value.GetType().GetProperty("message", BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); return messageProperty?.GetValue(value)?.ToString(); } private static string FlattenErrors(IDictionary errors) { var parts = errors .SelectMany(pair => pair.Value.Select(message => string.IsNullOrWhiteSpace(pair.Key) ? message : $"{pair.Key}: {message}")) .Where(message => !string.IsNullOrWhiteSpace(message)) .ToList(); return parts.Count == 0 ? "请求参数非法" : string.Join(";", parts); } private static bool ContainsDuplicateKey(Exception exception) { for (var current = exception; current != null; current = current.InnerException) { var message = current.Message; if (string.IsNullOrWhiteSpace(message)) continue; if (message.Contains("Duplicate entry", StringComparison.OrdinalIgnoreCase) || message.Contains("UNIQUE constraint failed", StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static ObjectResult Create(int statusCode, string code, string message) { return new ObjectResult(new AdoS0ApiErrorResponse { Code = code, Message = message }) { StatusCode = statusCode }; } } public sealed class AdoS0ResultFilter : IAsyncAlwaysRunResultFilter { public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { if (AdoS0ApiErrors.IsS0Request(context.HttpContext.Request.Path)) context.Result = AdoS0ApiErrors.WrapResult(context.Result); await next(); } } public sealed class AdoS0ExceptionFilter : IAsyncExceptionFilter { private readonly ILogger _logger; public AdoS0ExceptionFilter(ILogger logger) { _logger = logger; } public Task OnExceptionAsync(ExceptionContext context) { if (!AdoS0ApiErrors.TryMapUnhandledException(context.HttpContext.Request.Path, context.Exception, out var result) || result == null) return Task.CompletedTask; if (result.StatusCode == StatusCodes.Status409Conflict) _logger.LogWarning(context.Exception, "S0 duplicate conflict: {Path}", context.HttpContext.Request.Path); else _logger.LogError(context.Exception, "S0 unhandled exception: {Path}", context.HttpContext.Request.Path); context.Result = result; context.ExceptionHandled = true; return Task.CompletedTask; } }