feat: simplify transport warning to product+country with assessment SLA
AppKey-level scheme; scene matches one product and one country; stores assessment total hours (days×24) with migration SQL. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
dabb9a4edb
commit
aeae900b5e
31
scripts/sql/20260801_transport_warning_simplify.sql
Normal file
31
scripts/sql/20260801_transport_warning_simplify.sql
Normal file
@ -0,0 +1,31 @@
|
||||
-- 运输时效:一产品一国国家 + 考核总时效;规则主体按 AppKey 方案级
|
||||
-- 执行前请备份 t_warning / t_warning_transport_scene
|
||||
|
||||
-- 1) 国家列(若已存在可跳过)
|
||||
ALTER TABLE t_warning_transport_scene
|
||||
ADD COLUMN CountryCode VARCHAR(16) NULL COMMENT '国家二字码(单一)' AFTER ProductCode;
|
||||
|
||||
-- 2) 考核总时效(小时)
|
||||
ALTER TABLE t_warning_transport_scene
|
||||
ADD COLUMN AssessmentTotalTimeSpan FLOAT NULL COMMENT '考核总时效(小时)' AFTER CountryCode;
|
||||
|
||||
-- 3) 运输时效规则主体改为方案级:客户清空
|
||||
UPDATE t_warning
|
||||
SET Customer = ''
|
||||
WHERE WarningType = 1
|
||||
AND (Customer IS NULL OR Customer <> '');
|
||||
|
||||
-- 4) 同一 AppKey 下多个已启用运输时效方案,仅保留 Id 最小的一条
|
||||
UPDATE t_warning w
|
||||
INNER JOIN (
|
||||
SELECT AppKey, MIN(Id) AS KeepId
|
||||
FROM t_warning
|
||||
WHERE WarningType = 1
|
||||
AND IsEnabled = 1
|
||||
GROUP BY AppKey
|
||||
HAVING COUNT(1) > 1
|
||||
) d ON w.AppKey = d.AppKey
|
||||
SET w.IsEnabled = 0
|
||||
WHERE w.WarningType = 1
|
||||
AND w.IsEnabled = 1
|
||||
AND w.Id <> d.KeepId;
|
||||
@ -36,22 +36,32 @@ public class TransportWarningSceneDto
|
||||
public string? LeavePort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品编码(多个,分割)
|
||||
/// 产品编码(单一)
|
||||
/// </summary>
|
||||
public string? ProductCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 月份(多个/分割)
|
||||
/// 国家二字码(单一)
|
||||
/// </summary>
|
||||
public string? CountryCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 考核总时效(小时)
|
||||
/// </summary>
|
||||
public float? AssessmentTotalTimeSpan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 月份(多个/分割,历史字段)
|
||||
/// </summary>
|
||||
public string? Months { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域Id(单一)
|
||||
/// 区域Id(单一,历史字段)
|
||||
/// </summary>
|
||||
public int? AreaId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域id对应的名称
|
||||
/// 区域id对应的名称(历史字段)
|
||||
/// </summary>
|
||||
public string? AreaName { get; set; }
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TrackCenter.Application.Dtos.Warning.DataAnnotations;
|
||||
|
||||
namespace TrackCenter.Application.Dtos.Warning.Transport;
|
||||
@ -15,33 +16,31 @@ public sealed class UpsertTransportWarningSceneInput
|
||||
[IdRequired]
|
||||
public int WarningId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否中转(单一)
|
||||
/// </summary>
|
||||
/// <summary>历史字段,忽略</summary>
|
||||
public bool? IsTransit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 货物类型(多个,分割)
|
||||
/// </summary>
|
||||
/// <summary>历史字段,忽略</summary>
|
||||
public string? CargoType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 起运港口(多个,分割)
|
||||
/// </summary>
|
||||
/// <summary>历史字段,忽略</summary>
|
||||
public string? LeavePort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品编码(多个,分割)
|
||||
/// </summary>
|
||||
/// <summary>产品编码(单一销售产品)</summary>
|
||||
[Required(ErrorMessage = "请配置销售产品")]
|
||||
public string? ProductCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 月份(多个/分割)
|
||||
/// </summary>
|
||||
/// <summary>国家二字码(单一)</summary>
|
||||
[Required(ErrorMessage = "请配置国家")]
|
||||
public string? CountryCode { get; set; }
|
||||
|
||||
/// <summary>考核总时效(小时)</summary>
|
||||
[Required(ErrorMessage = "请配置考核总时效")]
|
||||
[Range(0.1, 99999, ErrorMessage = "考核总时效须大于0")]
|
||||
public float? AssessmentTotalTimeSpan { get; set; }
|
||||
|
||||
/// <summary>历史字段,忽略</summary>
|
||||
public string? Months { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域Id(单一)
|
||||
/// </summary>
|
||||
/// <summary>历史字段,忽略</summary>
|
||||
public int? AreaId { get; set; }
|
||||
}
|
||||
|
||||
@ -19,9 +19,9 @@ public class UpsertWarningRuleInput
|
||||
public string AppKey { get; set; } //应用
|
||||
|
||||
/// <summary>
|
||||
/// 客户
|
||||
/// 客户(运输时效可空,后端会置空;回传/完整预警必填)
|
||||
/// </summary>
|
||||
public string Customer { get; set; } //客户
|
||||
public string? Customer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 预警规则名称
|
||||
|
||||
@ -223,9 +223,9 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen
|
||||
orders = orders.Where(x => earliestTrackStatusTrackDict.ContainsKey(x.Id)).ToList();
|
||||
if (orders.Count == 0) return;
|
||||
|
||||
// 3.查规则主体
|
||||
var conditions = orders.Select(x => (AppKey: x.AppKey, x.Customer)).ToHashSet();
|
||||
var ruleDict = await _warningRuleRepository.GetWarningRuleForEnabledAsync(conditions, WarningRuleType.运输时效预警);
|
||||
// 3.查规则主体(运输时效:AppKey 方案级,不按客户)
|
||||
var appKeys = orders.Select(x => x.AppKey).ToHashSet();
|
||||
var ruleDict = await _warningRuleRepository.GetWarningRuleForEnabledByAppKeysAsync(appKeys, WarningRuleType.运输时效预警);
|
||||
if (ruleDict.Count == 0) return;
|
||||
|
||||
// 4.查规则场景
|
||||
@ -233,11 +233,7 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen
|
||||
var scenes = await _warningRuleRepository.GetTransportSceneAsync(x => ruleIds.Contains(x.WarningId));
|
||||
if (scenes.Count == 0) return;
|
||||
|
||||
// 5.查区域信息
|
||||
var areaIds = scenes.Where(x => x.AreaId > 0).Select(x => x.AreaId!.Value).ToHashSet();
|
||||
var ruleAreaInfosDict = await _kpiAreaRepository.GetAreaInfosDictByAreaId(areaIds);
|
||||
|
||||
// 选出订单命中的场景
|
||||
// 选出订单命中的场景(仅产品 + 国家)
|
||||
var hitSceneIds = new HashSet<int>();
|
||||
var orderIdHitSceneIdsDict = new Dictionary<int, HashSet<int>>();
|
||||
var sceneBos = TransportWarningSceneBO.From(scenes);
|
||||
@ -246,7 +242,10 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen
|
||||
var sceneSet = new HashSet<int>();
|
||||
foreach (var scene in sceneBos)
|
||||
{
|
||||
if (!scene.CanHitScene(order, ruleAreaInfosDict.GetValueOrDefault(scene.AreaId ?? 0))) continue;
|
||||
// 仅匹配同 AppKey 下方案的场景
|
||||
if (!ruleDict.TryGetValue(scene.WarningId, out var rule) || rule.AppKey != order.AppKey)
|
||||
continue;
|
||||
if (!scene.CanHitScene(order)) continue;
|
||||
sceneSet.Add(scene.Id);
|
||||
hitSceneIds.Add(scene.Id);
|
||||
}
|
||||
@ -576,14 +575,11 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen
|
||||
return;
|
||||
|
||||
// 3.重新验证订单是否命中新场景
|
||||
var areas = postScene.AreaId.HasValue
|
||||
? _kpiAreaRepository.GetAreaInfosByAreaId(postScene.AreaId.Value)
|
||||
: new List<KpiAreaInfo>();
|
||||
var noHitSceneOrderIds = new HashSet<int>();
|
||||
var reHitSceneOrderIds = new HashSet<int>();
|
||||
foreach (var order in orders)
|
||||
{
|
||||
if (postScene.CanHitScene(order, areas))
|
||||
if (postScene.CanHitScene(order))
|
||||
reHitSceneOrderIds.Add(order.Id);
|
||||
else
|
||||
noHitSceneOrderIds.Add(order.Id);
|
||||
@ -599,15 +595,11 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen
|
||||
var updateRuleItems = await _warningRuleRepository.GetTransportSceneRuleItemsAsync(x => updateRuleItemIds.Contains(x.Id));
|
||||
var ruleItemDict = updateRuleItems.ToDictionary(x => x.Id);
|
||||
|
||||
// 场景->区域
|
||||
var ruleArea = postScene.AreaId.HasValue
|
||||
? await _kpiAreaRepository.GetFirstAsync(x => x.Id == postScene.AreaId.Value)
|
||||
: null;
|
||||
foreach (var detail in updateDetails)
|
||||
{
|
||||
var ruleItem = ruleItemDict[detail.WarningSceneRuleItemId];
|
||||
detail.WarningName = rule.Name;
|
||||
detail.WarningRuleDescription = detail.GetWarningRuleDescription(postScene.ToEntity(), ruleItem, ruleArea);
|
||||
detail.WarningRuleDescription = detail.GetWarningRuleDescription(postScene.ToEntity(), ruleItem, null);
|
||||
needUpdateDetails.Add(detail);
|
||||
}
|
||||
}
|
||||
@ -679,12 +671,8 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen
|
||||
|
||||
private static bool CanSceneChange(TransportWarningSceneBO oldScene, TransportWarningSceneBO postScene)
|
||||
{
|
||||
return oldScene.CargoType != postScene.CargoType || //多个 普货,危险品
|
||||
oldScene.Months != postScene.Months || //多个 约束提交的月份组从小到大 1/3/4,不能出现 4/1/3这种 (如果出现则SequenceEqual)
|
||||
oldScene.AreaId != postScene.AreaId || // 单一,area1
|
||||
oldScene.IsTransit != postScene.IsTransit || //单一,true/false
|
||||
oldScene.LeavePort != postScene.LeavePort || //多个
|
||||
oldScene.ProductCode != postScene.ProductCode; //多个
|
||||
return !string.Equals(oldScene.ProductCode, postScene.ProductCode, StringComparison.OrdinalIgnoreCase)
|
||||
|| !string.Equals(oldScene.CountryCode, postScene.CountryCode, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
#endregion todo: 规则场景变更
|
||||
|
||||
@ -7,122 +7,64 @@ namespace TrackCenter.Application.Services.Warning.Model;
|
||||
|
||||
public sealed class TransportWarningSceneBO
|
||||
{
|
||||
/// <summary>
|
||||
/// 场景唯一id (传0视为新增)
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 预警规则主体id
|
||||
/// </summary>
|
||||
public int WarningId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 货物类型(多个,分割)
|
||||
/// </summary>
|
||||
public string? CargoType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 月份(多个/分割)
|
||||
/// </summary>
|
||||
public string? Months { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域Id(单一)
|
||||
/// </summary>
|
||||
public int? AreaId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否中转(单一)
|
||||
/// </summary>
|
||||
public bool? IsTransit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 起运港口(多个,分割)
|
||||
/// </summary>
|
||||
public string? LeavePort { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品编码(多个,分割)
|
||||
/// </summary>
|
||||
/// <summary>产品编码(单一)</summary>
|
||||
public string? ProductCode { get; }
|
||||
|
||||
public TransportWarningSceneBO(int id, int warningId, string? cargoType, string? months, int? areaId, bool? isTransit, string? leavePort, string? productCode)
|
||||
/// <summary>国家二字码(单一)</summary>
|
||||
public string? CountryCode { get; }
|
||||
|
||||
/// <summary>考核总时效(小时)</summary>
|
||||
public float? AssessmentTotalTimeSpan { get; }
|
||||
|
||||
public TransportWarningSceneBO(
|
||||
int id,
|
||||
int warningId,
|
||||
string? cargoType,
|
||||
string? months,
|
||||
int? areaId,
|
||||
bool? isTransit,
|
||||
string? leavePort,
|
||||
string? productCode,
|
||||
string? countryCode = null,
|
||||
float? assessmentTotalTimeSpan = null)
|
||||
{
|
||||
Id = id;
|
||||
WarningId = warningId;
|
||||
CargoType = cargoType;
|
||||
|
||||
// 如果有月份则排序之后在写入
|
||||
if (!string.IsNullOrWhiteSpace(months))
|
||||
{
|
||||
var sorted = months.Split('/').Select(int.Parse).OrderBy(x => x).ToList();
|
||||
Months = string.Join('/', sorted);
|
||||
}
|
||||
|
||||
Months = months;
|
||||
AreaId = areaId;
|
||||
IsTransit = isTransit;
|
||||
LeavePort = leavePort;
|
||||
ProductCode = productCode;
|
||||
ProductCode = string.IsNullOrWhiteSpace(productCode) ? null : productCode.Trim();
|
||||
CountryCode = NormalizeSingleCountryCode(countryCode);
|
||||
AssessmentTotalTimeSpan = assessmentTotalTimeSpan;
|
||||
}
|
||||
|
||||
public bool CanHitScene(OrderEntity order, List<KpiAreaInfo>? ruleAreas)
|
||||
/// <summary>
|
||||
/// 命中:订单产品 = 配置产品;到达国家 = 配置国家(一对一)。
|
||||
/// </summary>
|
||||
public bool CanHitScene(OrderEntity order, List<KpiAreaInfo>? ruleAreas = null)
|
||||
{
|
||||
// 是否中转
|
||||
if (IsTransit != null && IsTransit != order.IsTransit)
|
||||
_ = ruleAreas;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ProductCode) || string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
return false;
|
||||
|
||||
// 货物类型
|
||||
if (!string.IsNullOrWhiteSpace(CargoType) && order.CargoType != CargoType)
|
||||
if (!string.Equals(order.ProductCode.Trim(), ProductCode, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
// 启运港口
|
||||
if (!string.IsNullOrWhiteSpace(LeavePort) && order.LeavePort != LeavePort)
|
||||
if (string.IsNullOrWhiteSpace(CountryCode) || string.IsNullOrWhiteSpace(order.ArrivalCountry))
|
||||
return false;
|
||||
|
||||
// 产品编码
|
||||
if (!string.IsNullOrWhiteSpace(ProductCode) && order.ProductCode != ProductCode)
|
||||
return false;
|
||||
|
||||
// 月份
|
||||
if (!string.IsNullOrWhiteSpace(Months))
|
||||
{
|
||||
var months = string.IsNullOrEmpty(Months)
|
||||
? new HashSet<int>()
|
||||
: Months.Split('/').Select(int.Parse).ToHashSet();
|
||||
// 订单创建时间月份
|
||||
var orderCreateTimeMonth = order.CreateTime.Month;
|
||||
if (!months.Contains(orderCreateTimeMonth))
|
||||
return false;
|
||||
}
|
||||
|
||||
// 区域
|
||||
if (ruleAreas != null && ruleAreas.Count > 0)
|
||||
{
|
||||
// 使用订单到达国家的二字码匹配区域
|
||||
var areas = ruleAreas.Where(area => order.ArrivalCountry == area.CountryCode).ToList();
|
||||
foreach (var area in areas)
|
||||
{
|
||||
// 邮编是否在关注的范围内?
|
||||
if (!string.IsNullOrWhiteSpace(area.LeftPostcode) && !string.IsNullOrWhiteSpace(area.RightPostcode))
|
||||
{
|
||||
// tips: 参考小包的邮编匹配,字典序
|
||||
if (string.CompareOrdinal(order.ArrivalPostCode, area.LeftPostcode) < 0 ||
|
||||
string.CompareOrdinal(order.ArrivalPostCode, area.RightPostcode) > 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
// 城市是否匹配?
|
||||
if (!string.IsNullOrWhiteSpace(area.City) && order.ArrivalCity != area.City)
|
||||
return false;
|
||||
|
||||
// 省份是否匹配?
|
||||
if (!string.IsNullOrWhiteSpace(area.Province) && order.ArrivalProvince != area.Province)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return string.Equals(order.ArrivalCountry.Trim(), CountryCode, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static TransportWarningSceneEntity ToEntity(TransportWarningSceneBO bo)
|
||||
@ -135,23 +77,13 @@ public sealed class TransportWarningSceneBO
|
||||
bo.AreaId,
|
||||
bo.IsTransit,
|
||||
bo.LeavePort,
|
||||
bo.ProductCode
|
||||
bo.ProductCode,
|
||||
bo.CountryCode,
|
||||
bo.AssessmentTotalTimeSpan
|
||||
);
|
||||
}
|
||||
|
||||
public TransportWarningSceneEntity ToEntity()
|
||||
{
|
||||
return new TransportWarningSceneEntity(
|
||||
Id,
|
||||
WarningId,
|
||||
CargoType,
|
||||
Months,
|
||||
AreaId,
|
||||
IsTransit,
|
||||
LeavePort,
|
||||
ProductCode
|
||||
);
|
||||
}
|
||||
public TransportWarningSceneEntity ToEntity() => ToEntity(this);
|
||||
|
||||
public static TransportWarningSceneBO From(TransportWarningSceneEntity entity)
|
||||
{
|
||||
@ -163,47 +95,54 @@ public sealed class TransportWarningSceneBO
|
||||
entity.AreaId,
|
||||
entity.IsTransit,
|
||||
entity.LeavePort,
|
||||
entity.ProductCode
|
||||
entity.ProductCode,
|
||||
entity.CountryCode,
|
||||
entity.AssessmentTotalTimeSpan
|
||||
);
|
||||
}
|
||||
|
||||
public static List<TransportWarningSceneBO> From(List<TransportWarningSceneEntity> entities)
|
||||
{
|
||||
return entities.Select(From).ToList();
|
||||
}
|
||||
=> entities.Select(From).ToList();
|
||||
|
||||
public static TransportWarningSceneBO From(UpsertTransportWarningSceneInput input)
|
||||
{
|
||||
return new TransportWarningSceneBO(
|
||||
input.Id,
|
||||
input.WarningId,
|
||||
input.CargoType,
|
||||
input.Months,
|
||||
input.AreaId,
|
||||
input.IsTransit,
|
||||
input.LeavePort,
|
||||
input.ProductCode
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
input.ProductCode,
|
||||
input.CountryCode,
|
||||
input.AssessmentTotalTimeSpan
|
||||
);
|
||||
}
|
||||
|
||||
private bool Equals(TransportWarningSceneBO other)
|
||||
{
|
||||
return WarningId == other.WarningId
|
||||
&& IsTransit == other.IsTransit
|
||||
&& CargoType == other.CargoType
|
||||
&& LeavePort == other.LeavePort
|
||||
&& ProductCode == other.ProductCode
|
||||
&& Months == other.Months
|
||||
&& AreaId == other.AreaId;
|
||||
&& string.Equals(ProductCode, other.ProductCode, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(CountryCode, other.CountryCode, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || obj is TransportWarningSceneBO other && Equals(other);
|
||||
}
|
||||
=> ReferenceEquals(this, obj) || obj is TransportWarningSceneBO other && Equals(other);
|
||||
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(WarningId, ProductCode?.ToUpperInvariant(), CountryCode?.ToUpperInvariant());
|
||||
|
||||
private static string? NormalizeSingleCountryCode(string? countryCode)
|
||||
{
|
||||
return HashCode.Combine(WarningId, IsTransit, CargoType, LeavePort, ProductCode, Months, AreaId);
|
||||
if (string.IsNullOrWhiteSpace(countryCode))
|
||||
return null;
|
||||
|
||||
var code = countryCode
|
||||
.Split([',', '/', ';', '|', ' '], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(x => x.ToUpperInvariant())
|
||||
.FirstOrDefault(x => x.Length > 0);
|
||||
|
||||
return string.IsNullOrWhiteSpace(code) ? null : code;
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +89,8 @@ public class TransportWarningService : ApplicationServiceBase
|
||||
CargoType = x.CargoType,
|
||||
LeavePort = x.LeavePort,
|
||||
ProductCode = x.ProductCode,
|
||||
CountryCode = x.CountryCode,
|
||||
AssessmentTotalTimeSpan = x.AssessmentTotalTimeSpan,
|
||||
Months = x.Months,
|
||||
AreaId = x.AreaId,
|
||||
AreaName = x.AreaId.HasValue ? areaNameDict?.GetValueOrDefault(x.AreaId.Value) : null
|
||||
@ -96,37 +98,17 @@ public class TransportWarningService : ApplicationServiceBase
|
||||
|
||||
return map.ToList();
|
||||
|
||||
// 构建场景摘要
|
||||
// 构建场景摘要:产品 / 国家 / 总时效Xh
|
||||
string BuildSummary(TransportWarningSceneEntity entity)
|
||||
{
|
||||
const string separator = "/";
|
||||
// var sb = new StringBuilder();
|
||||
var list = new List<string>(6);
|
||||
// 货物类型
|
||||
if (!string.IsNullOrWhiteSpace(entity.CargoType))
|
||||
list.Add($"{entity.CargoType}");
|
||||
// 月份
|
||||
if (!string.IsNullOrWhiteSpace(entity.Months))
|
||||
list.Add($"{entity.Months.Replace("/", ",")}月份");
|
||||
// 区域名称
|
||||
if (entity.AreaId != null && areaNameDict != null)
|
||||
{
|
||||
var areaName = areaNameDict.GetValueOrDefault(entity.AreaId.Value);
|
||||
if (!string.IsNullOrWhiteSpace(areaName))
|
||||
list.Add($"{areaName}");
|
||||
}
|
||||
|
||||
// 中转
|
||||
if (entity.IsTransit != null)
|
||||
list.Add($"中转为{(entity.IsTransit.Value ? "是" : "否")}");
|
||||
// 起运港口
|
||||
if (!string.IsNullOrWhiteSpace(entity.LeavePort))
|
||||
list.Add($"{entity.LeavePort}");
|
||||
// 产品编码
|
||||
var list = new List<string>(3);
|
||||
if (!string.IsNullOrWhiteSpace(entity.ProductCode))
|
||||
list.Add($"{entity.ProductCode}");
|
||||
|
||||
return string.Join(separator, list);
|
||||
list.Add(entity.ProductCode);
|
||||
if (!string.IsNullOrWhiteSpace(entity.CountryCode))
|
||||
list.Add(entity.CountryCode);
|
||||
if (entity.AssessmentTotalTimeSpan is > 0)
|
||||
list.Add($"{entity.AssessmentTotalTimeSpan.Value / 24f:0.##}天");
|
||||
return list.Count == 0 ? "-" : string.Join(" / ", list);
|
||||
}
|
||||
}
|
||||
|
||||
@ -282,14 +264,31 @@ public class TransportWarningService : ApplicationServiceBase
|
||||
|
||||
var postBo = TransportWarningSceneBO.From(input);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(postBo.ProductCode))
|
||||
throw new BusinessException(message: "请配置销售产品");
|
||||
if (postBo.ProductCode.Contains(',') || postBo.ProductCode.Contains('/'))
|
||||
throw new BusinessException(message: "运输时效仅支持单一销售产品");
|
||||
if (string.IsNullOrWhiteSpace(postBo.CountryCode))
|
||||
throw new BusinessException(message: "请配置国家");
|
||||
if (input.CountryCode != null &&
|
||||
input.CountryCode.IndexOfAny([',', '/', ';', '|', ' ']) >= 0)
|
||||
throw new BusinessException(message: "运输时效仅支持单一国家");
|
||||
if (postBo.AssessmentTotalTimeSpan is null or <= 0)
|
||||
throw new BusinessException(message: "请配置有效的考核总时效");
|
||||
// 前端按整数天录入后转小时(天×24)
|
||||
var totalHours = postBo.AssessmentTotalTimeSpan.Value;
|
||||
if (Math.Abs(totalHours - MathF.Round(totalHours)) > 0.001f ||
|
||||
Math.Abs(totalHours % 24f) > 0.001f)
|
||||
throw new BusinessException(message: "考核总时效须为整天对应的小时数(天×24)");
|
||||
|
||||
// 查出这个规则主体下的所有场景
|
||||
var oldScenes = await _warningRuleRepository.GetTransportSceneAsync(input.WarningId);
|
||||
var oldBos = oldScenes.Count > 0 ? TransportWarningSceneBO.From(oldScenes) : Empty<TransportWarningSceneBO>.List;
|
||||
|
||||
// 验证规则场景是否重复
|
||||
// 验证规则场景是否重复(同一方案下:产品+国家唯一)
|
||||
if (oldBos.Any(old => old.Equals(postBo) && old.Id != postBo.Id))
|
||||
{
|
||||
throw new BusinessException(message: "同一规则主体下,不允许出现因素完全相同的场景!");
|
||||
throw new BusinessException(message: "同一方案下,不允许出现产品与国家完全相同的配置!");
|
||||
}
|
||||
|
||||
// 更新或者新增
|
||||
|
||||
@ -84,10 +84,13 @@ public class WarningService : ApplicationServiceBase
|
||||
throw new BusinessException(message: $"预警规则主体启用状态未发生变化,不允许重复操作");
|
||||
}
|
||||
|
||||
// 如果设置为启用则检查同一个应用同一客户下有多个启用的规则
|
||||
// 启用查重:运输时效按 AppKey+类型;其它类型按 AppKey+客户+类型
|
||||
if (input.IsEnabled && await _warningRuleRepository.CheckEnableDuplicateAsync(rule.Id, rule.AppKey, rule.Customer, (WarningRuleType) rule.WarningType))
|
||||
{
|
||||
throw new BusinessException(message: $"同一【应用+客户+类型】下不能有多个启用的预警规则主体,AppKey=[{rule.AppKey},Customer=[{rule.Customer}]]");
|
||||
var scope = (WarningRuleType) rule.WarningType == WarningRuleType.运输时效预警
|
||||
? $"同一【应用+类型】下不能有多个启用的预警规则主体,AppKey=[{rule.AppKey}]"
|
||||
: $"同一【应用+客户+类型】下不能有多个启用的预警规则主体,AppKey=[{rule.AppKey},Customer=[{rule.Customer}]]";
|
||||
throw new BusinessException(message: scope);
|
||||
}
|
||||
|
||||
// 更新规则主体状态
|
||||
@ -120,17 +123,28 @@ public class WarningService : ApplicationServiceBase
|
||||
throw new BusinessException(message: $"WarningId=[{input.Id}],WarningType=[{input.WarningType}]预警规则主体不存在,不允许修改");
|
||||
}
|
||||
|
||||
// 要求 AppKey+Customer+Name+WarningType 联合不能重复
|
||||
if (await _warningRuleRepository.CheckNameDuplicateAsync(input.Id, input.AppKey, input.Customer, input.Name, (WarningRuleType) input.WarningType))
|
||||
// 运输时效方案级:客户固定为空,不参与匹配
|
||||
var customer = (WarningRuleType) input.WarningType == WarningRuleType.运输时效预警
|
||||
? string.Empty
|
||||
: (input.Customer ?? string.Empty);
|
||||
|
||||
if ((WarningRuleType) input.WarningType != WarningRuleType.运输时效预警 && string.IsNullOrWhiteSpace(customer))
|
||||
throw new BusinessException(message: "客户不能为空");
|
||||
|
||||
// 名称查重:运输时效按 AppKey+Name+Type;其它按 AppKey+Customer+Name+Type
|
||||
if (await _warningRuleRepository.CheckNameDuplicateAsync(input.Id, input.AppKey, customer, input.Name, (WarningRuleType) input.WarningType))
|
||||
{
|
||||
throw new BusinessException(message: $"AppKey:[{input.AppKey}],Customer:[{input.Customer}]," +
|
||||
throw new BusinessException(message: $"AppKey:[{input.AppKey}],Customer:[{customer}]," +
|
||||
$"WarningType:[{input.WarningType.ToString()}]已存在名称为[{input.Name}]规则,不允许重复创建/修改");
|
||||
}
|
||||
|
||||
// 如果创建/修改的主体为启用状态,则要求通过检查
|
||||
if (input.IsEnabled && await _warningRuleRepository.CheckEnableDuplicateAsync(input.Id, input.AppKey, input.Customer, (WarningRuleType) input.WarningType))
|
||||
if (input.IsEnabled && await _warningRuleRepository.CheckEnableDuplicateAsync(input.Id, input.AppKey, customer, (WarningRuleType) input.WarningType))
|
||||
{
|
||||
throw new BusinessException(message: $"同一【应用+客户+预警类型】下不能有多个启用的预警规则主体,不允许创建/修改");
|
||||
var scope = (WarningRuleType) input.WarningType == WarningRuleType.运输时效预警
|
||||
? "同一【应用+预警类型】下不能有多个启用的预警规则主体,不允许创建/修改"
|
||||
: "同一【应用+客户+预警类型】下不能有多个启用的预警规则主体,不允许创建/修改";
|
||||
throw new BusinessException(message: scope);
|
||||
}
|
||||
|
||||
var enabledChanged = false;
|
||||
@ -140,7 +154,7 @@ public class WarningService : ApplicationServiceBase
|
||||
input.Id,
|
||||
input.Name,
|
||||
input.AppKey,
|
||||
input.Customer,
|
||||
customer,
|
||||
input.IsEnabled,
|
||||
input.WarningType
|
||||
);
|
||||
@ -150,7 +164,7 @@ public class WarningService : ApplicationServiceBase
|
||||
enabledChanged = ruleEntity.IsEnabled != input.IsEnabled;
|
||||
ruleEntity.Name = input.Name;
|
||||
ruleEntity.AppKey = input.AppKey;
|
||||
ruleEntity.Customer = input.Customer;
|
||||
ruleEntity.Customer = customer;
|
||||
ruleEntity.IsEnabled = input.IsEnabled;
|
||||
ruleEntity.WarningType = input.WarningType;
|
||||
}
|
||||
|
||||
@ -32,6 +32,11 @@ public interface IWarningRuleRepository : IBasicRepository<WarningRuleEntity>
|
||||
// 获取启用的预警规则主体
|
||||
Task<IReadOnlyDictionary<int, WarningRuleEntity>> GetWarningRuleForEnabledAsync(HashSet<(string AppKey, string Customer)> conditions, WarningRuleType warningType);
|
||||
|
||||
/// <summary>
|
||||
/// 按 AppKey 获取启用的预警规则主体(运输时效:方案级,不按客户)
|
||||
/// </summary>
|
||||
Task<IReadOnlyDictionary<int, WarningRuleEntity>> GetWarningRuleForEnabledByAppKeysAsync(HashSet<string> appKeys, WarningRuleType warningType);
|
||||
|
||||
// 获取预警规则主体根据id
|
||||
Task<WarningRuleEntity?> GetWarningRuleByIdAsync(int id, WarningRuleType? warningType = null);
|
||||
|
||||
|
||||
@ -155,6 +155,12 @@ public sealed class TransportWarningDetailsIndex : IdBaseIndex
|
||||
// 产品代码(xxx)
|
||||
if (!string.IsNullOrWhiteSpace(scene.ProductCode))
|
||||
res.Append($",{scene.ProductCode}");
|
||||
// 国家(US)
|
||||
if (!string.IsNullOrWhiteSpace(scene.CountryCode))
|
||||
res.Append($",{scene.CountryCode}");
|
||||
// 考核总时效
|
||||
if (scene.AssessmentTotalTimeSpan is > 0)
|
||||
res.Append($",考核总时效{scene.AssessmentTotalTimeSpan.Value / 24f:0.##}天");
|
||||
|
||||
// 规则配置信息
|
||||
res.Append($",生成预警时间{ruleItem.StartTrackWarningTimeSpan}小时" +
|
||||
|
||||
@ -13,29 +13,45 @@ public sealed class TransportWarningSceneEntity : FullAuditedEntity<int, int>
|
||||
[SugarColumn(ColumnName = "WarningId", ColumnDescription = "预警规则id")]
|
||||
public int WarningId { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "IsTransit", ColumnDescription = "是否中转")]
|
||||
public bool? IsTransit { get; set; } //是否中转
|
||||
[SugarColumn(ColumnName = "IsTransit", ColumnDescription = "是否中转", IsNullable = true)]
|
||||
public bool? IsTransit { get; set; } //历史字段
|
||||
|
||||
[SugarColumn(ColumnName = "CargoType", ColumnDescription = "货物类型")]
|
||||
public string? CargoType { get; set; } //货物类型,
|
||||
[SugarColumn(ColumnName = "CargoType", ColumnDescription = "货物类型", IsNullable = true)]
|
||||
public string? CargoType { get; set; } //历史字段
|
||||
|
||||
[SugarColumn(ColumnName = "LeavePort", ColumnDescription = "起运港口")]
|
||||
public string? LeavePort { get; set; } //起运港口
|
||||
[SugarColumn(ColumnName = "LeavePort", ColumnDescription = "起运港口", IsNullable = true)]
|
||||
public string? LeavePort { get; set; } //历史字段
|
||||
|
||||
[SugarColumn(ColumnName = "ProductCode", ColumnDescription = "产品编码")]
|
||||
public string? ProductCode { get; set; } //产品编码
|
||||
[SugarColumn(ColumnName = "ProductCode", ColumnDescription = "产品编码", IsNullable = true)]
|
||||
public string? ProductCode { get; set; } //单一销售产品
|
||||
|
||||
[SugarColumn(ColumnName = "Months", ColumnDescription = "月份")]
|
||||
public string? Months { get; set; } //月份, 1/2/3
|
||||
[SugarColumn(ColumnName = "CountryCode", ColumnDescription = "国家二字码(单一)", IsNullable = true, Length = 16)]
|
||||
public string? CountryCode { get; set; } //单一国家二字码
|
||||
|
||||
[SugarColumn(ColumnName = "AreaId", ColumnDescription = "区域id")]
|
||||
public int? AreaId { get; set; } //区域Id,单分区
|
||||
[SugarColumn(ColumnName = "AssessmentTotalTimeSpan", ColumnDescription = "考核总时效(小时)", IsNullable = true)]
|
||||
public float? AssessmentTotalTimeSpan { get; set; } //考核总时效
|
||||
|
||||
[SugarColumn(ColumnName = "Months", ColumnDescription = "月份", IsNullable = true)]
|
||||
public string? Months { get; set; } //历史字段
|
||||
|
||||
[SugarColumn(ColumnName = "AreaId", ColumnDescription = "区域id", IsNullable = true)]
|
||||
public int? AreaId { get; set; } //历史字段
|
||||
|
||||
public TransportWarningSceneEntity()
|
||||
{
|
||||
}
|
||||
|
||||
public TransportWarningSceneEntity(int id, int warningId, string? cargoType, string? months, int? areaId, bool? isTransit, string? leavePort, string? productCode)
|
||||
public TransportWarningSceneEntity(
|
||||
int id,
|
||||
int warningId,
|
||||
string? cargoType,
|
||||
string? months,
|
||||
int? areaId,
|
||||
bool? isTransit,
|
||||
string? leavePort,
|
||||
string? productCode,
|
||||
string? countryCode = null,
|
||||
float? assessmentTotalTimeSpan = null)
|
||||
{
|
||||
Id = id;
|
||||
WarningId = warningId;
|
||||
@ -45,5 +61,7 @@ public sealed class TransportWarningSceneEntity : FullAuditedEntity<int, int>
|
||||
IsTransit = isTransit;
|
||||
LeavePort = leavePort;
|
||||
ProductCode = productCode;
|
||||
CountryCode = countryCode;
|
||||
AssessmentTotalTimeSpan = assessmentTotalTimeSpan;
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,12 +15,16 @@ public class WarningRuleRepository : BasicRepository<WarningRuleEntity>, IWarnin
|
||||
|
||||
public async Task<bool> CheckNameDuplicateAsync(int id, string appKey, string customer, string name, WarningRuleType warningType)
|
||||
{
|
||||
var entity = await Context.Queryable<WarningRuleEntity>()
|
||||
// 运输时效:AppKey + Name + Type(方案级,不按客户)
|
||||
var query = Context.Queryable<WarningRuleEntity>()
|
||||
.Where(x => x.AppKey == appKey
|
||||
&& x.Customer == customer
|
||||
&& x.WarningType == (int) warningType
|
||||
&& string.Equals(x.Name, name))
|
||||
.FirstAsync();
|
||||
&& string.Equals(x.Name, name));
|
||||
|
||||
if (warningType != WarningRuleType.运输时效预警)
|
||||
query = query.Where(x => x.Customer == customer);
|
||||
|
||||
var entity = await query.FirstAsync();
|
||||
|
||||
// 不重复
|
||||
if (entity == null)
|
||||
@ -32,14 +36,18 @@ public class WarningRuleRepository : BasicRepository<WarningRuleEntity>, IWarnin
|
||||
|
||||
public async Task<bool> CheckEnableDuplicateAsync(int warningId, string appKey, string customer, WarningRuleType warningType)
|
||||
{
|
||||
var entity = await Context.Queryable<WarningRuleEntity>()
|
||||
// 运输时效:同一 AppKey + 类型仅允许一个启用方案
|
||||
var query = Context.Queryable<WarningRuleEntity>()
|
||||
.Where(x => x.AppKey == appKey
|
||||
&& x.Customer == customer
|
||||
&& x.WarningType == (int) warningType
|
||||
&& x.IsEnabled)
|
||||
.FirstAsync();
|
||||
&& x.IsEnabled);
|
||||
|
||||
// 没有【应用+客户+类型】启用的规则
|
||||
if (warningType != WarningRuleType.运输时效预警)
|
||||
query = query.Where(x => x.Customer == customer);
|
||||
|
||||
var entity = await query.FirstAsync();
|
||||
|
||||
// 没有启用的规则
|
||||
if (entity == null) return false;
|
||||
|
||||
// 已经启用和当前规则不同,视为启用重复,不允许操作
|
||||
@ -117,6 +125,22 @@ public class WarningRuleRepository : BasicRepository<WarningRuleEntity>, IWarnin
|
||||
: list.ToDictionary(x => x.Id);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<int, WarningRuleEntity>> GetWarningRuleForEnabledByAppKeysAsync(HashSet<string> appKeys, WarningRuleType warningType)
|
||||
{
|
||||
if (appKeys.Count == 0)
|
||||
return Empty<int, WarningRuleEntity>.Dict;
|
||||
|
||||
var list = await Context.Queryable<WarningRuleEntity>()
|
||||
.Where(x => x.WarningType == (int) warningType
|
||||
&& x.IsEnabled
|
||||
&& appKeys.Contains(x.AppKey))
|
||||
.ToListAsync();
|
||||
|
||||
return list.Count == 0
|
||||
? Empty<int, WarningRuleEntity>.Dict
|
||||
: list.ToDictionary(x => x.Id);
|
||||
}
|
||||
|
||||
public async Task<WarningRuleEntity?> GetWarningRuleByIdAsync(int id, WarningRuleType? warningType = null)
|
||||
{
|
||||
var result = await Context.Queryable<WarningRuleEntity>()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user