From 41f4c58e99609a5db5c1822c725cedc17bda3165 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Tue, 28 Jul 2026 19:30:27 +0800 Subject: [PATCH] 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 --- db.sql | 28 +-- sql/20260728_order_file.sql | 40 ++-- sql/20260728_order_file_comments.sql | 20 ++ .../Dtos/Orders/OrderParcelDraftFailData.cs | 35 ++++ .../Orders/OrderService.ParcelBilling.cs | 178 +++++++++++++++++- .../Services/Orders/OrderService.cs | 14 +- .../OmsException2ResponseConverter.cs | 141 ++++++++++++++ src/SimpleAdmin.Api/SimpleAdminApiModule.cs | 14 ++ 8 files changed, 430 insertions(+), 40 deletions(-) create mode 100644 sql/20260728_order_file_comments.sql create mode 100644 src/Oms.Application/Dtos/Orders/OrderParcelDraftFailData.cs create mode 100644 src/SimpleAdmin.Api/ExceptionHandling/OmsException2ResponseConverter.cs diff --git a/db.sql b/db.sql index c8afb68..b1afb07 100644 --- a/db.sql +++ b/db.sql @@ -179,22 +179,22 @@ CREATE TABLE `t_order` ( ) COMMENT='订单表'; CREATE TABLE `t_order_file` ( - `Id` INT NOT NULL AUTO_INCREMENT, + `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增', `OrderId` INT NOT NULL COMMENT '订单Id', - `FileType` VARCHAR(32) NOT NULL COMMENT 'tradeProof/systemLabel/downstreamLabel/lastMileLabel/infraredImage', + `FileType` VARCHAR(32) NOT NULL COMMENT '附件类型:tradeProof交易证明/systemLabel系统面单/downstreamLabel下游面单/lastMileLabel末端面单/infraredImage红外图片', `FileName` VARCHAR(255) NULL COMMENT '原始文件名', - `FileUrl` VARCHAR(512) NOT NULL COMMENT '相对路径或外链', - `ContentType` VARCHAR(128) NULL, - `FileSize` BIGINT NULL, - `Sort` INT NOT NULL DEFAULT 0, - `Source` VARCHAR(32) NOT NULL DEFAULT 'upload' COMMENT 'upload/systemGenerate/downstream/migrate', - `Remark` VARCHAR(512) NULL, - `IsDeleted` TINYINT NOT NULL DEFAULT 0, - `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()), - `CreateUserId` INT NOT NULL DEFAULT 0, - `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - `UpdateUserId` INT NULL, - `UpdateTime` DATETIME NULL, + `FileUrl` VARCHAR(512) NOT NULL COMMENT '文件地址(相对路径或外链)', + `ContentType` VARCHAR(128) NULL COMMENT '文件MIME类型', + `FileSize` BIGINT NULL COMMENT '文件大小(字节)', + `Sort` INT NOT NULL DEFAULT 0 COMMENT '同类型内排序', + `Source` VARCHAR(32) NOT NULL DEFAULT 'upload' COMMENT '来源:upload上传/systemGenerate系统生成/downstream下游回传/migrate迁移', + `Remark` VARCHAR(512) NULL COMMENT '备注', + `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', + `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳', + `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人', + `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `UpdateUserId` INT NULL COMMENT '更新人', + `UpdateTime` DATETIME NULL COMMENT '更新时间', PRIMARY KEY (`Id`), KEY `idx_ord_file_order_type` (`OrderId`, `FileType`, `IsDeleted`) ) COMMENT='订单附件表'; diff --git a/sql/20260728_order_file.sql b/sql/20260728_order_file.sql index 79d7970..1e94b5f 100644 --- a/sql/20260728_order_file.sql +++ b/sql/20260728_order_file.sql @@ -1,28 +1,28 @@ --- Order files table + migrate LabelUrl into systemLabel, then drop legacy columns. --- Run on MySQL. Backup first. +-- 订单附件表;将原 LabelUrl 迁移为 systemLabel 后删除订单旧面单字段。 +-- 在 MySQL 执行前请先备份。 CREATE TABLE IF NOT EXISTS `t_order_file` ( - `Id` INT NOT NULL AUTO_INCREMENT, - `OrderId` INT NOT NULL COMMENT 'Order Id', - `FileType` VARCHAR(32) NOT NULL COMMENT 'tradeProof/systemLabel/downstreamLabel/lastMileLabel/infraredImage', - `FileName` VARCHAR(255) NULL COMMENT 'Original file name', - `FileUrl` VARCHAR(512) NOT NULL COMMENT 'Relative or absolute URL', - `ContentType` VARCHAR(128) NULL, - `FileSize` BIGINT NULL, - `Sort` INT NOT NULL DEFAULT 0, - `Source` VARCHAR(32) NOT NULL DEFAULT 'upload' COMMENT 'upload/systemGenerate/downstream/migrate', - `Remark` VARCHAR(512) NULL, - `IsDeleted` TINYINT NOT NULL DEFAULT 0, - `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()), - `CreateUserId` INT NOT NULL DEFAULT 0, - `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - `UpdateUserId` INT NULL, - `UpdateTime` DATETIME NULL, + `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增', + `OrderId` INT NOT NULL COMMENT '订单Id', + `FileType` VARCHAR(32) NOT NULL COMMENT '附件类型:tradeProof交易证明/systemLabel系统面单/downstreamLabel下游面单/lastMileLabel末端面单/infraredImage红外图片', + `FileName` VARCHAR(255) NULL COMMENT '原始文件名', + `FileUrl` VARCHAR(512) NOT NULL COMMENT '文件地址(相对路径或外链)', + `ContentType` VARCHAR(128) NULL COMMENT '文件MIME类型', + `FileSize` BIGINT NULL COMMENT '文件大小(字节)', + `Sort` INT NOT NULL DEFAULT 0 COMMENT '同类型内排序', + `Source` VARCHAR(32) NOT NULL DEFAULT 'upload' COMMENT '来源:upload上传/systemGenerate系统生成/downstream下游回传/migrate迁移', + `Remark` VARCHAR(512) NULL COMMENT '备注', + `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', + `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳', + `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人', + `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `UpdateUserId` INT NULL COMMENT '更新人', + `UpdateTime` DATETIME NULL COMMENT '更新时间', PRIMARY KEY (`Id`), KEY `idx_ord_file_order_type` (`OrderId`, `FileType`, `IsDeleted`) -) COMMENT='Order attachment files'; +) COMMENT='订单附件表'; --- Migrate existing system label URL (ignore if column already dropped) +-- 迁移已有系统面单地址(若 LabelUrl 列已删除可跳过本段) INSERT INTO `t_order_file` (`OrderId`, `FileType`, `FileName`, `FileUrl`, `ContentType`, `Sort`, `Source`, `CreateUserId`, `CreateTime`) SELECT `Id`, diff --git a/sql/20260728_order_file_comments.sql b/sql/20260728_order_file_comments.sql new file mode 100644 index 0000000..1934c3c --- /dev/null +++ b/sql/20260728_order_file_comments.sql @@ -0,0 +1,20 @@ +-- 若 t_order_file 已按英文注释建表,执行本脚本改为中文注释(不改结构)。 + +ALTER TABLE `t_order_file` + MODIFY COLUMN `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增', + MODIFY COLUMN `OrderId` INT NOT NULL COMMENT '订单Id', + MODIFY COLUMN `FileType` VARCHAR(32) NOT NULL COMMENT '附件类型:tradeProof交易证明/systemLabel系统面单/downstreamLabel下游面单/lastMileLabel末端面单/infraredImage红外图片', + MODIFY COLUMN `FileName` VARCHAR(255) NULL COMMENT '原始文件名', + MODIFY COLUMN `FileUrl` VARCHAR(512) NOT NULL COMMENT '文件地址(相对路径或外链)', + MODIFY COLUMN `ContentType` VARCHAR(128) NULL COMMENT '文件MIME类型', + MODIFY COLUMN `FileSize` BIGINT NULL COMMENT '文件大小(字节)', + MODIFY COLUMN `Sort` INT NOT NULL DEFAULT 0 COMMENT '同类型内排序', + MODIFY COLUMN `Source` VARCHAR(32) NOT NULL DEFAULT 'upload' COMMENT '来源:upload上传/systemGenerate系统生成/downstream下游回传/migrate迁移', + MODIFY COLUMN `Remark` VARCHAR(512) NULL COMMENT '备注', + MODIFY COLUMN `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', + MODIFY COLUMN `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳', + MODIFY COLUMN `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人', + MODIFY COLUMN `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + MODIFY COLUMN `UpdateUserId` INT NULL COMMENT '更新人', + MODIFY COLUMN `UpdateTime` DATETIME NULL COMMENT '更新时间', + COMMENT='订单附件表'; diff --git a/src/Oms.Application/Dtos/Orders/OrderParcelDraftFailData.cs b/src/Oms.Application/Dtos/Orders/OrderParcelDraftFailData.cs new file mode 100644 index 0000000..d7bc198 --- /dev/null +++ b/src/Oms.Application/Dtos/Orders/OrderParcelDraftFailData.cs @@ -0,0 +1,35 @@ +namespace Oms.Application.Dtos.Orders; + +/// +/// 小包创建/草稿预报失败且已落库时的结构化错误载荷(写入异常 Data / 响应 Details)。 +/// +public class OrderParcelDraftFailData +{ + public const string ErrorCodeValue = "OMS_DRAFT_SAVED"; + public const string DataKey = "omsDraftFail"; + + /// + /// 是否已保存为草稿。 + /// + public bool DraftSaved { get; set; } = true; + + /// + /// 订单主键。 + /// + public int OrderId { get; set; } + + /// + /// 运单号。 + /// + public string ServiceNo { get; set; } = string.Empty; + + /// + /// 干净失败原因(不含机器标记)。 + /// + public string Reason { get; set; } = string.Empty; + + /// + /// 稳定错误码,供前端识别。 + /// + public string ErrorCode { get; set; } = ErrorCodeValue; +} diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs index d4e9afe..7d93f8f 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs @@ -181,16 +181,86 @@ public partial class OrderService } catch (BusinessException ex) { - await MarkParcelDraftFailedAsync(order, ex.Message); - throw; + var cleanReason = StripDraftSavedMarker(ex.Message); + await MarkParcelDraftFailedAsync(order, cleanReason); + throw CreateDraftSavedBusinessException(order, cleanReason); } catch (Exception ex) { - await MarkParcelDraftFailedAsync(order, ex.Message); - throw new BusinessException(ex.Message); + var cleanReason = StripDraftSavedMarker(ex.Message); + await MarkParcelDraftFailedAsync(order, cleanReason); + throw CreateDraftSavedBusinessException(order, cleanReason); } } + /// + /// 草稿已落库失败时,在业务文案末尾附加机器可读标记,供前端区分「未落库 / 已存草稿」。 + /// 格式:原因 [[omsDraft:{"draftSaved":true,"orderId":1,"serviceNo":"IDE..."}]] + /// 同时写入 Exception.Data / ErrorCode,由 OmsException2ResponseConverter 输出到响应 Details。 + /// + private const string DraftSavedMarkerPrefix = " [[omsDraft:"; + private const string DraftSavedMarkerSuffix = "]]"; + + 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 string WrapDraftSavedFail(Order order, string reason) + { + var clean = StripDraftSavedMarker(reason); + if (string.IsNullOrWhiteSpace(clean)) + { + clean = "预报未完成"; + } + + var payload = global::System.Text.Json.JsonSerializer.Serialize(new Dictionary + { + ["draftSaved"] = true, + ["orderId"] = order.Id, + ["serviceNo"] = order.ServiceNo ?? string.Empty, + ["reason"] = clean, + ["errorCode"] = OrderParcelDraftFailData.ErrorCodeValue, + }); + return $"{clean}{DraftSavedMarkerPrefix}{payload}{DraftSavedMarkerSuffix}"; + } + + /// + /// 抛出「草稿已落库」业务异常:ErrorCode + Data 结构化载荷,msg 仍带兼容标记。 + /// + private static BusinessException CreateDraftSavedBusinessException(Order order, string reason) + { + var clean = StripDraftSavedMarker(reason); + if (string.IsNullOrWhiteSpace(clean)) + { + clean = "预报未完成"; + } + + var failData = new OrderParcelDraftFailData + { + DraftSaved = true, + OrderId = order.Id, + ServiceNo = order.ServiceNo ?? string.Empty, + Reason = clean, + ErrorCode = OrderParcelDraftFailData.ErrorCodeValue, + }; + + var ex = new BusinessException(WrapDraftSavedFail(order, clean)); + ex.ErrorCode = OrderParcelDraftFailData.ErrorCodeValue; + ex.Data["draftSaved"] = true; + ex.Data["orderId"] = order.Id; + ex.Data["serviceNo"] = order.ServiceNo ?? string.Empty; + ex.Data[OrderParcelDraftFailData.DataKey] = + global::System.Text.Json.JsonSerializer.Serialize(failData); + return ex; + } + private const string ParcelServiceNoPrefix = "IDE"; /// @@ -1536,6 +1606,106 @@ public partial class OrderService throw new BusinessException("运单号生成失败"); } + /// + /// 草稿重新预报:按订单当前收件/件重尺/快照重跑黑名单、计重、分区与预报应收。 + /// 失败时保持草稿并写入 PredeclareFailReason。 + /// + internal async Task CompleteDraftPredeclareAsync(Order order, int? operatorUserId = null) + { + if (string.IsNullOrWhiteSpace(order.ProductCode)) + { + throw new BusinessException("订单未绑定销售产品,无法预报"); + } + + var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim()); + if (product == null || product.IsActive != EnableStatus.Enabled) + { + throw new BusinessException("销售产品不存在或未启用"); + } + + try + { + var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order); + var address = await ResolveOrderBillingAddressAsync(order); + + if (await IsBlacklistHitAsync( + product, + snapshotBinding.Rules, + address.CountryCode, + address.Province, + address.City, + address.ZipCode)) + { + throw new BusinessException("收件地址命中黑名单区域"); + } + + order.IsRemote = await IsRemoteHitAsync( + product, + snapshotBinding.Rules, + address.CountryCode, + address.Province, + address.City, + address.ZipCode) ? (byte)1 : (byte)0; + + var weights = (await OrderRepository.GetWeightsByOrderIdsAsync([order.Id])) + .OrderBy(x => x.Id) + .ToList(); + var forecast = weights.FirstOrDefault(x => + string.Equals(x.Type, OrderWeightTypeCodes.Forecast, StringComparison.OrdinalIgnoreCase)) + ?? throw new BusinessException("订单缺少预报件重尺"); + + await ApplyChargeableWeightAsync(product, snapshotBinding.Rules, forecast); + SyncWeightRows(weights, forecast); + await OrderRepository.ReplaceWeightsAsync(order.Id, weights); + + var billing = await ResolveOrderBillingContextAsync( + order, + product, + forecast.ChargeableWeight ?? 0, + snapshotBinding); + + await UpsertReceivableFeesAsync( + order, + billing, + OrderFeeChargingStageCodes.Forecast, + replaceExistingReceivable: true); + + order.OrderStatus = OrderStatusCodes.Predeclared; + order.PredeclareFailReason = string.Empty; + order.AllocationPendingReason = string.Empty; + order.UpdateTime = DateTime.Now; + await OrderRepository.UpdateAsync(order); + + var customer = order.CustomerId.HasValue + ? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value) + : null; + await EnqueueSmallBagTrackUpsertAsync(order, customer, receiver: null, appendInitialTrack: true); + await EnqueueGenerateLabelPdfAsync(order); + + try + { + var triggerUserId = operatorUserId ?? CurrentUser.GetId(); + await AutoTriggerRuntimeService.FireAsync(order, AutoTriggerTiming.OrderCreate, triggerUserId); + } + catch + { + // 自动触发失败不影响预报主流程 + } + } + catch (BusinessException ex) + { + var cleanReason = StripDraftSavedMarker(ex.Message); + await MarkParcelDraftFailedAsync(order, cleanReason); + throw CreateDraftSavedBusinessException(order, cleanReason); + } + catch (Exception ex) + { + var cleanReason = StripDraftSavedMarker(ex.Message); + await MarkParcelDraftFailedAsync(order, cleanReason); + throw CreateDraftSavedBusinessException(order, cleanReason); + } + } + private async Task MarkParcelDraftFailedAsync(Order order, string reason) { order.OrderStatus = OrderStatusCodes.Draft; diff --git a/src/Oms.Application/Services/Orders/OrderService.cs b/src/Oms.Application/Services/Orders/OrderService.cs index b8c8424..14f753e 100644 --- a/src/Oms.Application/Services/Orders/OrderService.cs +++ b/src/Oms.Application/Services/Orders/OrderService.cs @@ -193,8 +193,18 @@ public partial class OrderService : ApplicationService public async Task PreDeclareAsync(OrderActionInput input) { var order = await GetOrderOrThrowAsync(input.Id); - order.OrderStatus = OrderStatusCodes.Predeclared; - await UpdateOrderAsync(order); + if (string.Equals(order.OrderStatus, OrderStatusCodes.Predeclared, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + if (!string.Equals(order.OrderStatus, OrderStatusCodes.Draft, StringComparison.OrdinalIgnoreCase)) + { + throw new BusinessException( + $"订单状态为「{OmsDocHelper.GetOrderStatusName(order.OrderStatus)}」,不允许预报"); + } + + await CompleteDraftPredeclareAsync(order); } /// diff --git a/src/SimpleAdmin.Api/ExceptionHandling/OmsException2ResponseConverter.cs b/src/SimpleAdmin.Api/ExceptionHandling/OmsException2ResponseConverter.cs new file mode 100644 index 0000000..576a3b2 --- /dev/null +++ b/src/SimpleAdmin.Api/ExceptionHandling/OmsException2ResponseConverter.cs @@ -0,0 +1,141 @@ +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; + +/// +/// 在 Fake 默认异常转换之上,把草稿已落库失败的结构化信息写入 Details,并清理 msg 中的机器标记。 +/// +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(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(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; +} diff --git a/src/SimpleAdmin.Api/SimpleAdminApiModule.cs b/src/SimpleAdmin.Api/SimpleAdminApiModule.cs index 9cad2cb..9ae4d2c 100644 --- a/src/SimpleAdmin.Api/SimpleAdminApiModule.cs +++ b/src/SimpleAdmin.Api/SimpleAdminApiModule.cs @@ -101,6 +101,20 @@ public class SimpleAdminApiModule : FakeModule }); services.AddFeiShuExceptionSubscriber(); + + // 草稿已落库失败:结构化 Details + 清理 msg 机器标记 + services.AddTransient(); + var exceptionConverterDescriptors = services + .Where(d => d.ServiceType == typeof(IException2ResponseConverter)) + .ToList(); + foreach (var descriptor in exceptionConverterDescriptors) + { + services.Remove(descriptor); + } + + services.AddTransient(sp => + new ExceptionHandling.OmsException2ResponseConverter( + sp.GetRequiredService())); } public override void ConfigureApplication(ApplicationConfigureContext context)