From d0e202fbc6cf0bf0170e86bc99175f90d6f28d06 Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 21:21:02 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=BA=D0=B0=D0=B7=D1=8B=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B2=20=D0=B0=D1=83=D0=B4=D0=B8=D1=82?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D1=91=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit settings_updated пишет по каждому ключу «поле: было → стало» (скаляры и списки; секреты и снимки состояния — пометкой «изменено»), длинные значения обрезаются. --- .../Deal.Api/Endpoints/SettingsEndpoints.cs | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs b/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs index 7944ea9..3270eea 100644 --- a/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs @@ -5,6 +5,7 @@ using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Services; using Deal.Modules.Tenants.Application.Models; +using Deal.SharedKernel.Utilities; namespace Deal.Api.Endpoints; @@ -18,6 +19,15 @@ public static class SettingsEndpoints private const string SettingsOpenApiTag = "settings"; private const string InvalidBodyDetail = "Тело запроса должно быть JSON-объектом"; + // Максимальная длина значения в деталях аудита (промпты/списки бывают длинными). + private const int MaxAuditValueLength = 120; + + // Пометка для составных настроек, чьи значения в аудит не пишутся (секреты, снимки состояния). + private const string ChangedMarker = "изменено"; + + // Опции сериализации снимка настроек в деталях аудита (camelCase, как на wire). + private static readonly JsonSerializerOptions AuditJsonOptions = new(JsonSerializerDefaults.Web); + /// /// Регистрирует GET/PATCH /api/settings. /// @@ -72,9 +82,10 @@ public static class SettingsEndpoints } SettingsService settingsService = context.RequestServices.GetRequiredService(); + PublicSettingsDto before = await settingsService.GetPublicAsync(ct); PublicSettingsDto result = await settingsService.ApplyPatchAsync(body, ct); - await AuditAppender.AppendTenantAsync(context, AuditEvents.SettingsUpdated, [AuditDetails.Set(AuditFields.Fields, string.Join(", ", body.Keys))], ct); + await AuditAppender.AppendTenantAsync(context, AuditEvents.SettingsUpdated, BuildSettingsChanges(before, result, body.Keys), ct); if (ShouldScheduleRatesRefresh(body)) { @@ -84,6 +95,60 @@ public static class SettingsEndpoints return Results.Ok(result); } + // Строит изменения настроек «поле: было → стало» по применённому PATCH. + // before: Снимок настроек до применения. + // after: Снимок настроек после применения. + // keys: Ключи PATCH-тела (изменённые). + // Возвращает: Изменения по каждому ключу (составные — пометкой «изменено»). + private static IReadOnlyList BuildSettingsChanges( + PublicSettingsDto before, + PublicSettingsDto after, + IEnumerable keys) + { + using JsonDocument beforeDoc = JsonDocument.Parse(JsonSerializer.Serialize(before, AuditJsonOptions)); + using JsonDocument afterDoc = JsonDocument.Parse(JsonSerializer.Serialize(after, AuditJsonOptions)); + var changes = new List(); + var keyList = new List(); + foreach (string key in keys) + { + keyList.Add(key); + SettingKind? kind = SettingsKeys.FindPublicKind(key); + if (kind is SettingKind.Dict or SettingKind.MyPrompts or SettingKind.AiConfigs) + { + changes.Add(AuditDetails.Set(key, ChangedMarker)); + continue; + } + + changes.Add(AuditDetails.Change( + key, + ReadSettingText(beforeDoc.RootElement, key), + ReadSettingText(afterDoc.RootElement, key))); + } + + return changes.Count > 0 + ? changes + : [AuditDetails.Set(AuditFields.Fields, string.Join(", ", keyList))]; + } + + // Читает значение ключа снимка настроек как текст для деталей аудита. + private static string? ReadSettingText(JsonElement root, string key) => + root.TryGetProperty(key, out JsonElement value) ? ToText(value) : null; + + // Приводит значение JSON к строке отображения (длинные значения обрезаются). + private static string? ToText(JsonElement value) => value.ValueKind switch + { + JsonValueKind.Null or JsonValueKind.Undefined => null, + JsonValueKind.String => Truncate(value.GetString()), + JsonValueKind.True => BoolText.True, + JsonValueKind.False => BoolText.False, + JsonValueKind.Array => Truncate(string.Join(", ", value.EnumerateArray().Select(ToText))), + _ => Truncate(value.GetRawText()), + }; + + // Обрезает длинное значение деталей аудита. + private static string? Truncate(string? value) => + value is not null && value.Length > MaxAuditValueLength ? value[..MaxAuditValueLength] + "…" : value; + private static bool ShouldScheduleRatesRefresh(Dictionary body) { if (!body.TryGetValue(SettingsKeys.RateSource, out JsonElement element))