Files
Deal/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs
T
stepan 1fbe29a838 Вынести магические строки статусов, видов и ключей в каталоги
Литералы статусов/видов/фаз и wire-ключей заменены каталогами: значения сущностей и репозиториев — TenantStatuses/TenantLimitPeriods/InviteStatuses/ContainerKinds/ContainerSpaces/CardIds/Discovery*/PipelineQueueStatuses; контакты и виды вложений — CardContactTypes/CardFileKinds; источники и этапы отсева — PipelineRejectStages/PipelineRejectSources; ключи промптов/провайдеров — SettingsFieldKeys; wire-ключи канбана — KanbanWireKeys; режимы правил — ColumnRuleModes; операции/фазы/виды диалогов/контакты Telegram и общий BoolText — в Deal.Contracts/Deal.SharedKernel; имена метрик — DealMetrics. Значения не менялись.
2026-09-13 20:51:36 +03:00

253 lines
9.5 KiB
C#

using System.Globalization;
using System.Text.Json;
using Deal.Modules.Settings.Application.Models;
using Deal.SharedKernel.Utilities;
namespace Deal.Modules.Settings.Application.Services;
public sealed partial class SettingsService
{
private async Task<Dictionary<string, JsonElement>> LoadStoredOverridesAsync(CancellationToken ct)
{
var result = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
IReadOnlyCollection<SettingValue> rows = await store.GetAllAsync(ct);
foreach (SettingValue row in rows)
{
try
{
using JsonDocument document = JsonDocument.Parse(row.ValueJson);
result[row.Key] = document.RootElement.Clone();
}
catch (JsonException)
{
// Повреждённая строка не должна ронять снимок: значение трактуется как отсутствующее (дефолт).
}
}
return result;
}
private static bool TryReadInt(JsonElement element, out int value)
{
long wide;
if (element.ValueKind == JsonValueKind.Number)
{
if (!element.TryGetInt64(out wide))
{
value = 0;
return false;
}
}
else if (element.ValueKind == JsonValueKind.String)
{
if (!long.TryParse(element.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out wide))
{
value = 0;
return false;
}
}
else
{
value = 0;
return false;
}
// Диапазон long шире int: сжимаем (клампы PATCH всё равно ограничены диапазоном меньше int32).
value = (int)Math.Clamp(wide, int.MinValue, int.MaxValue);
return true;
}
private static bool TryReadBool(JsonElement element, out bool value)
{
if (element.ValueKind is JsonValueKind.True or JsonValueKind.False)
{
value = element.GetBoolean();
return true;
}
value = false;
return false;
}
private static bool TryReadText(JsonElement element, out string? text)
{
switch (element.ValueKind)
{
case JsonValueKind.String:
text = element.GetString();
return true;
case JsonValueKind.Number:
text = element.GetRawText();
return true;
case JsonValueKind.True:
text = BoolText.True;
return true;
case JsonValueKind.False:
text = BoolText.False;
return true;
default:
text = null;
return false;
}
}
// Читает строковое поле объекта (JSON null / отсутствие / не-скаляр → False).
// obj: JSON-объект.
// name: Имя поля.
// text: Значение поля.
// Возвращает: True — поле присутствует и скалярно.
private static bool TryReadFieldText(
JsonElement obj,
string name,
out string text)
{
if (obj.TryGetProperty(name, out JsonElement element) && TryReadText(element, out string? value) && value is not null)
{
text = value;
return true;
}
text = string.Empty;
return false;
}
// Кламп целого в диапазон.
// value: Исходное значение.
// min: Нижняя граница.
// max: Верхняя граница.
// Возвращает: Значение в диапазоне [min..max].
private static int Clamp(
int value,
int min,
int max) => Math.Max(min, Math.Min(max, value));
// Объединяет дефолт и переопределение целочисленного ключа.
private static int MergeInt(
Dictionary<string, JsonElement> overrides,
string key,
int defaultValue)
{
return overrides.TryGetValue(key, out JsonElement element) && TryReadInt(element, out int value)
? value
: defaultValue;
}
// Объединяет дефолт и переопределение булева ключа.
private static bool MergeBool(
Dictionary<string, JsonElement> overrides,
string key,
bool defaultValue)
{
return overrides.TryGetValue(key, out JsonElement element) && TryReadBool(element, out bool value)
? value
: defaultValue;
}
// Объединяет дефолт и переопределение строкового ключа.
private static string MergeString(
Dictionary<string, JsonElement> overrides,
string key,
string defaultValue)
{
return overrides.TryGetValue(key, out JsonElement element)
&& TryReadText(element, out string? value)
&& value is not null
? value
: defaultValue;
}
// Объединяет дефолт и переопределение списка строк.
private static IReadOnlyList<string> MergeStringList(
Dictionary<string, JsonElement> overrides,
string key,
IReadOnlyList<string> defaultValue)
{
if (overrides.TryGetValue(key, out JsonElement element) && element.ValueKind == JsonValueKind.Array)
{
var items = new List<string>();
foreach (JsonElement item in element.EnumerateArray())
{
if (TryReadText(item, out string? text) && text is not null)
{
items.Add(text);
}
}
return items;
}
return defaultValue;
}
// Объединяет дефолт и переопределение словаря (colState): значения сохраняются как JsonElement.
private static IReadOnlyDictionary<string, object?> MergeDict(Dictionary<string, JsonElement> overrides, string key)
{
if (overrides.TryGetValue(key, out JsonElement element) && element.ValueKind == JsonValueKind.Object)
{
var result = new Dictionary<string, object?>(StringComparer.Ordinal);
foreach (JsonProperty property in element.EnumerateObject())
{
result[property.Name] = property.Value.Clone();
}
return result;
}
return new Dictionary<string, object?>(StringComparer.Ordinal);
}
// Объединяет дефолты aiConfigs и сохранённое переопределение (переопределение — на провайдера).
// defaults: Дефолтные конфиги на всех провайдеров каталога.
// overrides: Сохранённые переопределения.
// Возвращает: Эффективные конфиги: провайдеры дефолтов + перекрытия из хранилища (неизвестные пропускаются).
private static IReadOnlyDictionary<string, AiConfigSetting> MergeAiConfigs(IReadOnlyDictionary<string, AiConfigSetting> defaults, Dictionary<string, JsonElement> overrides)
{
var merged = defaults.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
if (overrides.TryGetValue(SettingsKeys.AiConfigs, out JsonElement element) && element.ValueKind == JsonValueKind.Object)
{
foreach (JsonProperty provider in element.EnumerateObject())
{
// «Только существующие провайдеры»: неизвестные id из хранилища не подмешиваются.
if (!merged.ContainsKey(provider.Name) || provider.Value.ValueKind != JsonValueKind.Object)
{
continue;
}
string apiKey = TryReadFieldText(provider.Value, SettingsFieldKeys.ApiKey, out string storedKey) ? storedKey : string.Empty;
string baseUrl = TryReadFieldText(provider.Value, SettingsFieldKeys.BaseUrl, out string storedBase) ? storedBase : string.Empty;
string model = TryReadFieldText(provider.Value, SettingsFieldKeys.Model, out string storedModel) ? storedModel : string.Empty;
merged[provider.Name] = new AiConfigSetting(apiKey, baseUrl, model);
}
}
return merged;
}
// Читает сохранённые «Мои промпты»; повреждённое значение → пустой список (дефолт).
// overrides: Сохранённые переопределения.
// Возвращает: Список промптов из хранилища или дефолт (пустой).
private static IReadOnlyList<MyPromptDto> MergeMyPrompts(Dictionary<string, JsonElement> overrides)
{
if (overrides.TryGetValue(SettingsKeys.MyPrompts, out JsonElement element) && element.ValueKind == JsonValueKind.Array)
{
try
{
return JsonSerializer.Deserialize<List<MyPromptDto>>(element.GetRawText(), JsonOptions)
?? new List<MyPromptDto>();
}
catch (JsonException)
{
return Array.Empty<MyPromptDto>();
}
}
return SettingsDefaults.MyPrompts;
}
}