Несколько условий шага, объединяемых через И/ИЛИ
Раньше шаг маршрута мог нести ровно одно условие (field_name/operation/ value). Теперь условий может быть сколько угодно, а их объединение задаётся явно — ConditionsLogic.And (все условия) или .Or (любое из условий). Бэкенд: - Новая сущность TemplateRouteStepCondition (дочерняя таблица template_route_step_conditions, FK на шаг, cascade delete) вместо трёх колонок на TemplateRouteStep. Миграция AddMultipleStepConditions переносит существующие условия шагов в новую таблицу перед удалением старых колонок, чтобы не потерять уже настроенные в деве/проде условия. - TemplateRouteService.MatchesConditions: несколько условий шага объединяются по ConditionsLogic (Any/All); HasUnevaluatableManagerCondition проверяет условие на IsManager в списке условий, а не единственное поле. - TemplateRouteStepService: дубликаты шагов теперь сравниваются по всему набору условий (без учёта порядка) и, если условий два и более, по логике их объединения — сравнение в памяти, а не в SQL, кандидатов на дубликат в пределах маршрута всегда мало. - Валидация условия: раньше «все три поля или ни одного», теперь — в каждом элементе списка все три поля обязательны. Фронтенд: - RouteStepFormModal: условие — динамический список строк (добавить/удалить), у каждой строки свой набор допустимых операторов по типу колонки; селектор И/ИЛИ появляется, когда условий два и больше. - describeStepCondition в таблице шагов склеивает все условия через « И »/« ИЛИ ». Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -189,7 +189,7 @@ public sealed class TemplateRouteService(
|
||||
|
||||
var matchingSteps = route.Steps
|
||||
.Where(s => s.Status == request.Status)
|
||||
.Where(s => MatchesCondition(s, fieldValues) || HasUnevaluatableManagerCondition(s, positionMissing))
|
||||
.Where(s => MatchesConditions(s, fieldValues) || HasUnevaluatableManagerCondition(s, positionMissing))
|
||||
.OrderBy(s => s.OrderNo)
|
||||
.ToList();
|
||||
|
||||
@@ -220,7 +220,7 @@ public sealed class TemplateRouteService(
|
||||
private async Task<TemplateRoute?> FindRouteAsync(
|
||||
int requestTypeId, Guid? companyId, Guid? departmentId, bool byProject, CancellationToken cancellationToken) =>
|
||||
await db.TemplateRoutes.AsNoTracking()
|
||||
.Include(x => x.Steps)
|
||||
.Include(x => x.Steps).ThenInclude(s => s.Conditions)
|
||||
.Where(x =>
|
||||
x.RequestTypeId == requestTypeId &&
|
||||
x.CompanyId == companyId &&
|
||||
@@ -228,7 +228,7 @@ public sealed class TemplateRouteService(
|
||||
x.DepartmentId == departmentId)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
?? await db.TemplateRoutes.AsNoTracking()
|
||||
.Include(x => x.Steps)
|
||||
.Include(x => x.Steps).ThenInclude(s => s.Conditions)
|
||||
.Where(x =>
|
||||
x.RequestTypeId == requestTypeId &&
|
||||
x.CompanyId == companyId &&
|
||||
@@ -344,21 +344,29 @@ public sealed class TemplateRouteService(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Условие шага (field_name/operation/value) против значений полей, переданных в resolve-запросе.
|
||||
/// Шаг без условия применяется всегда. Шаг с условием, для которого не передано значение поля
|
||||
/// (fieldValues его не содержит), считается неприменимым — при resolve мы моделируем конкретный
|
||||
/// сценарий, и отсутствие значения означает, что условие не выполняется.
|
||||
/// Условия шага против значений полей, переданных в resolve-запросе. Шаг без условий
|
||||
/// применяется всегда. Несколько условий объединяются через <see cref="TemplateRouteStep.ConditionsLogic"/>:
|
||||
/// И — должны выполниться все, ИЛИ — достаточно одного. Условие, для которого не передано
|
||||
/// значение поля (fieldValues его не содержит), считается невыполненным — при resolve мы
|
||||
/// моделируем конкретный сценарий, и отсутствие значения означает, что условие не выполняется.
|
||||
/// </summary>
|
||||
private static bool MatchesCondition(TemplateRouteStep step, IReadOnlyDictionary<string, string> fieldValues)
|
||||
private static bool MatchesConditions(TemplateRouteStep step, IReadOnlyDictionary<string, string> fieldValues)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(step.FieldName) || step.Operation is null or ComparisonOperator.None)
|
||||
if (step.Conditions.Count == 0)
|
||||
return true;
|
||||
|
||||
if (!fieldValues.TryGetValue(step.FieldName, out var actual) || string.IsNullOrWhiteSpace(actual))
|
||||
return step.ConditionsLogic == ConditionsLogic.Or
|
||||
? step.Conditions.Any(c => MatchesCondition(c, fieldValues))
|
||||
: step.Conditions.All(c => MatchesCondition(c, fieldValues));
|
||||
}
|
||||
|
||||
private static bool MatchesCondition(TemplateRouteStepCondition condition, IReadOnlyDictionary<string, string> fieldValues)
|
||||
{
|
||||
if (!fieldValues.TryGetValue(condition.FieldName, out var actual) || string.IsNullOrWhiteSpace(actual))
|
||||
return false;
|
||||
|
||||
var expected = step.Value;
|
||||
return step.Operation switch
|
||||
var expected = condition.Value;
|
||||
return condition.Operation switch
|
||||
{
|
||||
ComparisonOperator.Equal => IsEqual(actual, expected),
|
||||
ComparisonOperator.NotEqual => !IsEqual(actual, expected),
|
||||
@@ -399,14 +407,14 @@ public sealed class TemplateRouteService(
|
||||
"поэтому шаг включён в маршрут, хотя условие могло и не выполниться.";
|
||||
|
||||
/// <summary>
|
||||
/// Условие шага завязано на IsManager, но вычислить его не из чего: должность
|
||||
/// в заявке не передана. Такой шаг попадает в результат вопреки условию и с
|
||||
/// предупреждением — см. ResolveAsync.
|
||||
/// Одно из условий шага завязано на IsManager, но вычислить его не из чего: должность
|
||||
/// в заявке не передана. Такой шаг попадает в результат вопреки условиям и с
|
||||
/// предупреждением — см. ResolveAsync. Не различаем И/ИЛИ и остальные условия шага:
|
||||
/// раз про IsManager мы не знаем правду, итоговый матч шага в принципе ненадёжен.
|
||||
/// </summary>
|
||||
private static bool HasUnevaluatableManagerCondition(TemplateRouteStep step, bool positionMissing) =>
|
||||
positionMissing &&
|
||||
string.Equals(step.FieldName, ManagerConditionField, StringComparison.OrdinalIgnoreCase) &&
|
||||
step.Operation is not (null or ComparisonOperator.None);
|
||||
step.Conditions.Any(c => string.Equals(c.FieldName, ManagerConditionField, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Должность — свободный текст без структуры (нет отдельного флага «это
|
||||
|
||||
Reference in New Issue
Block a user