From 1af42903139a567f9c68b1cb6b138a9cd673a1d4 Mon Sep 17 00:00:00 2001 From: nns Date: Sun, 7 Jun 2026 23:09:09 +0500 Subject: [PATCH] =?UTF-8?q?fix(s22):=201C-CSV=20detect=20charset=20=D0=B8?= =?UTF-8?q?=D0=B7=20Content-Type=20+=20UpdatedAt=20=D0=B2=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Parse1cCsv ловил Windows-1251 на UTF-8 bodies без BOM. Теперь смотрит Content-Type charset первым, потом BOM, потом WIN-1251. - Migration Phase22a_OrgExports забыл UpdatedAt колонку (Entity base имеет её). ADD COLUMN IF NOT EXISTS внутри миграции для уже созданных таблиц. Co-Authored-By: Claude Opus 4.7 --- .../Controllers/Catalog/ProductsController.cs | 49 +++++++++++-------- .../20260607220000_Phase22a_OrgExports.cs | 8 ++- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/food-market.api/Controllers/Catalog/ProductsController.cs b/src/food-market.api/Controllers/Catalog/ProductsController.cs index d6faac4..ffdafd9 100644 --- a/src/food-market.api/Controllers/Catalog/ProductsController.cs +++ b/src/food-market.api/Controllers/Catalog/ProductsController.cs @@ -574,33 +574,40 @@ public record OneCImportResponse(int Created, int Skipped, IReadOnlyListДетект кодировки: BOM UTF-8 → utf-8; иначе предполагаем - /// Windows-1251 (стандарт 1С). После чтения первых байтов перематываем - /// поток в начало. - private static System.Text.Encoding DetectEncoding(Stream s) + /// Детект кодировки: 1) Content-Type charset (если указан), + /// 2) BOM UTF-8 — utf-8, 3) fallback Windows-1251 (стандарт 1С). + /// После чтения первых байтов перематываем поток в начало. + private System.Text.Encoding DetectEncoding(Stream s) { - try - { - // Регистрируем codepage providers (Windows-1251 не в default-пуле .NET 8). - System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); - } + try { System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); } catch { /* идемпотентно */ } - if (!s.CanSeek) - return new System.Text.UTF8Encoding(true); - var pos = s.Position; - var buf = new byte[3]; - var n = s.Read(buf, 0, 3); - s.Position = pos; - if (n >= 3 && buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF) - return System.Text.Encoding.UTF8; - try + + // 1. Content-Type charset. + var ct = Request.ContentType; + if (!string.IsNullOrEmpty(ct)) { - return System.Text.Encoding.GetEncoding("windows-1251"); + var m = System.Text.RegularExpressions.Regex.Match(ct, @"charset=([^;\s]+)", System.Text.RegularExpressions.RegexOptions.IgnoreCase); + if (m.Success) + { + try { return System.Text.Encoding.GetEncoding(m.Groups[1].Value); } + catch { /* fall through */ } + } } - catch + + // 2. BOM-based detect. + if (s.CanSeek) { - return System.Text.Encoding.UTF8; + var pos = s.Position; + var buf = new byte[3]; + var n = s.Read(buf, 0, 3); + s.Position = pos; + if (n >= 3 && buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF) + return System.Text.Encoding.UTF8; } + + // 3. Fallback — Windows-1251 (стандарт 1С). + try { return System.Text.Encoding.GetEncoding("windows-1251"); } + catch { return System.Text.Encoding.UTF8; } } /// Парсер 1С-CSV → CsvProductRow. Возвращает (rows, errors). diff --git a/src/food-market.infrastructure/Persistence/Migrations/20260607220000_Phase22a_OrgExports.cs b/src/food-market.infrastructure/Persistence/Migrations/20260607220000_Phase22a_OrgExports.cs index e30c286..fba5a86 100644 --- a/src/food-market.infrastructure/Persistence/Migrations/20260607220000_Phase22a_OrgExports.cs +++ b/src/food-market.infrastructure/Persistence/Migrations/20260607220000_Phase22a_OrgExports.cs @@ -30,9 +30,15 @@ protected override void Up(MigrationBuilder b) ""DownloadTokenExpiresAt"" timestamp with time zone NULL, ""NotifyEmail"" varchar(200) NULL, ""Error"" varchar(2000) NULL, - ""CreatedAt"" timestamp with time zone NOT NULL DEFAULT now() + ""CreatedAt"" timestamp with time zone NOT NULL DEFAULT now(), + ""UpdatedAt"" timestamp with time zone NULL ); + -- Идемпотентно — для случая когда таблица уже создана без UpdatedAt + -- (миграция была запущена до её добавления в схему). + ALTER TABLE public.org_exports + ADD COLUMN IF NOT EXISTS ""UpdatedAt"" timestamp with time zone NULL; + CREATE INDEX IF NOT EXISTS ""IX_org_exports_Org_CreatedAt"" ON public.org_exports (""OrganizationId"", ""CreatedAt"" DESC); CREATE UNIQUE INDEX IF NOT EXISTS ""IX_org_exports_DownloadToken""