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>
This commit is contained in:
parent
03f11a276f
commit
41f4c58e99
28
db.sql
28
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='订单附件表';
|
||||
|
||||
@ -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`,
|
||||
|
||||
20
sql/20260728_order_file_comments.sql
Normal file
20
sql/20260728_order_file_comments.sql
Normal file
@ -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='订单附件表';
|
||||
35
src/Oms.Application/Dtos/Orders/OrderParcelDraftFailData.cs
Normal file
35
src/Oms.Application/Dtos/Orders/OrderParcelDraftFailData.cs
Normal file
@ -0,0 +1,35 @@
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 小包创建/草稿预报失败且已落库时的结构化错误载荷(写入异常 Data / 响应 Details)。
|
||||
/// </summary>
|
||||
public class OrderParcelDraftFailData
|
||||
{
|
||||
public const string ErrorCodeValue = "OMS_DRAFT_SAVED";
|
||||
public const string DataKey = "omsDraftFail";
|
||||
|
||||
/// <summary>
|
||||
/// 是否已保存为草稿。
|
||||
/// </summary>
|
||||
public bool DraftSaved { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 订单主键。
|
||||
/// </summary>
|
||||
public int OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 运单号。
|
||||
/// </summary>
|
||||
public string ServiceNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 干净失败原因(不含机器标记)。
|
||||
/// </summary>
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 稳定错误码,供前端识别。
|
||||
/// </summary>
|
||||
public string ErrorCode { get; set; } = ErrorCodeValue;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 草稿已落库失败时,在业务文案末尾附加机器可读标记,供前端区分「未落库 / 已存草稿」。
|
||||
/// 格式:原因 [[omsDraft:{"draftSaved":true,"orderId":1,"serviceNo":"IDE..."}]]
|
||||
/// 同时写入 Exception.Data / ErrorCode,由 OmsException2ResponseConverter 输出到响应 Details。
|
||||
/// </summary>
|
||||
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<string, object>
|
||||
{
|
||||
["draftSaved"] = true,
|
||||
["orderId"] = order.Id,
|
||||
["serviceNo"] = order.ServiceNo ?? string.Empty,
|
||||
["reason"] = clean,
|
||||
["errorCode"] = OrderParcelDraftFailData.ErrorCodeValue,
|
||||
});
|
||||
return $"{clean}{DraftSavedMarkerPrefix}{payload}{DraftSavedMarkerSuffix}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 抛出「草稿已落库」业务异常:ErrorCode + Data 结构化载荷,msg 仍带兼容标记。
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
@ -1536,6 +1606,106 @@ public partial class OrderService
|
||||
throw new BusinessException("运单号生成失败");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 草稿重新预报:按订单当前收件/件重尺/快照重跑黑名单、计重、分区与预报应收。
|
||||
/// 失败时保持草稿并写入 PredeclareFailReason。
|
||||
/// </summary>
|
||||
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<int>();
|
||||
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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -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;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
@ -101,6 +101,20 @@ public class SimpleAdminApiModule : FakeModule
|
||||
});
|
||||
|
||||
services.AddFeiShuExceptionSubscriber();
|
||||
|
||||
// 草稿已落库失败:结构化 Details + 清理 msg 机器标记
|
||||
services.AddTransient<DefaultException2ResponseConverter>();
|
||||
var exceptionConverterDescriptors = services
|
||||
.Where(d => d.ServiceType == typeof(IException2ResponseConverter))
|
||||
.ToList();
|
||||
foreach (var descriptor in exceptionConverterDescriptors)
|
||||
{
|
||||
services.Remove(descriptor);
|
||||
}
|
||||
|
||||
services.AddTransient<IException2ResponseConverter>(sp =>
|
||||
new ExceptionHandling.OmsException2ResponseConverter(
|
||||
sp.GetRequiredService<DefaultException2ResponseConverter>()));
|
||||
}
|
||||
|
||||
public override void ConfigureApplication(ApplicationConfigureContext context)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user