fix(s22): 1C-CSV detect charset из Content-Type + UpdatedAt в migration
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions

- 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 <noreply@anthropic.com>
This commit is contained in:
nns 2026-06-07 23:09:09 +05:00
parent 4c1ac37a08
commit 1af4290313
2 changed files with 35 additions and 22 deletions

View file

@ -574,33 +574,40 @@ public record OneCImportResponse(int Created, int Skipped, IReadOnlyList<CsvImpo
return new OneCImportResponse(v.Created, rows.Count - v.Created, v.Errors, v.Ids); return new OneCImportResponse(v.Created, rows.Count - v.Created, v.Errors, v.Ids);
} }
/// <summary>Детект кодировки: BOM UTF-8 → utf-8; иначе предполагаем /// <summary>Детект кодировки: 1) Content-Type charset (если указан),
/// Windows-1251 (стандарт 1С). После чтения первых байтов перематываем /// 2) BOM UTF-8 — utf-8, 3) fallback Windows-1251 (стандарт 1С).
/// поток в начало.</summary> /// После чтения первых байтов перематываем поток в начало.</summary>
private static System.Text.Encoding DetectEncoding(Stream s) private System.Text.Encoding DetectEncoding(Stream s)
{ {
try try { System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); }
{
// Регистрируем codepage providers (Windows-1251 не в default-пуле .NET 8).
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
}
catch { /* идемпотентно */ } catch { /* идемпотентно */ }
if (!s.CanSeek)
return new System.Text.UTF8Encoding(true); // 1. Content-Type charset.
var pos = s.Position; var ct = Request.ContentType;
var buf = new byte[3]; if (!string.IsNullOrEmpty(ct))
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
{ {
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; }
} }
/// <summary>Парсер 1С-CSV → CsvProductRow. Возвращает (rows, errors). /// <summary>Парсер 1С-CSV → CsvProductRow. Возвращает (rows, errors).

View file

@ -30,9 +30,15 @@ protected override void Up(MigrationBuilder b)
""DownloadTokenExpiresAt"" timestamp with time zone NULL, ""DownloadTokenExpiresAt"" timestamp with time zone NULL,
""NotifyEmail"" varchar(200) NULL, ""NotifyEmail"" varchar(200) NULL,
""Error"" varchar(2000) 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"" CREATE INDEX IF NOT EXISTS ""IX_org_exports_Org_CreatedAt""
ON public.org_exports (""OrganizationId"", ""CreatedAt"" DESC); ON public.org_exports (""OrganizationId"", ""CreatedAt"" DESC);
CREATE UNIQUE INDEX IF NOT EXISTS ""IX_org_exports_DownloadToken"" CREATE UNIQUE INDEX IF NOT EXISTS ""IX_org_exports_DownloadToken""