using System.Globalization;
using MS.Microservice.Infrastructure.Utils.Excel;
namespace MS.Microservice.Infrastructure.Utils;
public partial class ExcelHelper
{
/// Reads a header-first CSV using the same model mapping and validation as Excel reports.
/// Leaves input open. Report row numbers are logical CSV records; SheetName is CSV.
/// MaximumRows bounds data records, including invalid rows. Sheet/row initialization applies only to Excel.
public async Task> ImportCsvWithReportAsync(Stream source,
Func>? validateRow = null,
CsvOptions? options = null, int maximumRows = 10000, CancellationToken cancellationToken = default)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumRows);
cancellationToken.ThrowIfCancellationRequested();
var meta = GetOrCreateTypeMeta(typeof(T));
await using var records = CsvHelper.ReadAsync(source, options, cancellationToken).GetAsyncEnumerator(cancellationToken);
if (!await records.MoveNextAsync().ConfigureAwait(false))
throw new InvalidDataException("CSV 必须包含表头。");
var headers = records.Current.Fields;
var map = headers.Select(name => meta.NameToSlotIndex.TryGetValue(name.Trim(), out var index) ? index : -1).ToArray();
var errors = ValidateReportColumns(meta, map, map.Length, "CSV", 0);
var rows = new List>();
if (errors.Count != 0) return new(rows.AsReadOnly(), errors.AsReadOnly());
while (await records.MoveNextAsync().ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
var record = records.Current;
if (record.RowNumber - 1 > maximumRows || record.RowNumber > int.MaxValue)
throw new InvalidDataException("CSV 数据记录数超过 maximumRows。");
if (record.Fields.Count != headers.Count)
throw new CsvFormatException("字段数必须与表头一致。", record.RowNumber,
Math.Min(record.Fields.Count, headers.Count) + 1);
var rowIndex = (int)record.RowNumber - 1;
var value = (T)meta.Factory();
var errorCount = errors.Count;
for (var column = 0; column < map.Length; column++)
{
if (map[column] < 0) continue;
var slot = meta.Slots[map[column]];
var text = record.Fields[column];
if (string.IsNullOrWhiteSpace(text))
{
if (slot.Required)
errors.Add(CreateTableImportError("CSV", ExcelImportErrorKind.Required, rowIndex, column,
slot, "必填值不能为空。", text));
}
else if (TryConvertValue(text, slot.TargetTypeCode, slot.TargetType, out var converted))
{
// User setters are application code; their exceptions must propagate.
slot.Setter(value!, converted);
}
else
errors.Add(CreateTableImportError("CSV", ExcelImportErrorKind.Conversion, rowIndex, column,
slot, $"无法转换为 {slot.TargetType.Name}。", text));
}
if (errors.Count != errorCount) continue;
if (validateRow is not null)
ValidateBusinessRow(value, validateRow, meta, map, map.Length, rowIndex, "CSV", errors, cancellationToken);
if (errors.Count == errorCount) rows.Add(new((int)record.RowNumber, value));
}
cancellationToken.ThrowIfCancellationRequested();
return new(rows.AsReadOnly(), errors.AsReadOnly());
}
/// Writes model columns and rows as CSV, reusing ExcelColumn names, order and ignore settings.
/// Leaves output open. Values use invariant formatting; DateTime uses the round-trip format.
/// Like CsvHelper, this preserves field text and does not sanitize spreadsheet formulas.
public Task ExportCsvAsync(IEnumerable source, Stream destination, CsvOptions? options = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(source);
cancellationToken.ThrowIfCancellationRequested();
var meta = GetOrCreateTypeMeta(typeof(T));
return CsvHelper.WriteAsync(destination, Records(), options, cancellationToken);
IEnumerable> Records()
{
yield return meta.Slots.Select(slot => (string?)slot.ColumnName).ToArray();
foreach (var row in source)
{
cancellationToken.ThrowIfCancellationRequested();
ArgumentNullException.ThrowIfNull(row);
yield return meta.Slots.Select(slot => slot.Getter(row!) switch
{
null => null,
DateTime date => date.ToString("O", CultureInfo.InvariantCulture),
IFormattable formatted => formatted.ToString(null, CultureInfo.InvariantCulture),
object value => value.ToString()
}).ToArray();
}
}
}
}