Skip to content

Commit f27f28f

Browse files
committed
fix: 多项问题修复与功能改进
将 WebClient 替换为 HttpClient,增加 30s 超时防止请求无限挂起;新增 DateOnly/DateOnly? 类型导入支持 (#588);修复 CSV 导入空单元格映射为 null (#585);修复模板导入空行导致错误行号偏移 (#582);移除 net5.0/netcoreapp3.1 目标框架;升级 Newtonsoft.Json、DependencyModel 安全版本;PdfExporter 属性迁移适配;补充多项集成测试
1 parent 051bc08 commit f27f28f

18 files changed

Lines changed: 819 additions & 55 deletions

src/Magicodes.ExporterAndImporter.AspNetCore/Magicodes.IE.AspNetCore.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<FrameworkReference Include="Microsoft.AspNetCore.App" />
99
</ItemGroup>
1010
<ItemGroup>
11-
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
11+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
1212
</ItemGroup>
1313
<ItemGroup>
1414
<ProjectReference Include="..\Magicodes.ExporterAndImporter.Excel\Magicodes.IE.Excel.csproj" />

src/Magicodes.ExporterAndImporter.Core/Magicodes.IE.Core.csproj

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<Import Project="..\..\common.props"></Import>
33
<PropertyGroup>
4-
<TargetFrameworks>netstandard2.0;netstandard2.1;net5.0;net6.0;net7.0;net8.0;net10.0</TargetFrameworks>
4+
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net10.0</TargetFrameworks>
55
<PackageId>Magicodes.IE.Core</PackageId>
66
</PropertyGroup>
77
<ItemGroup>
@@ -10,22 +10,17 @@
1010
<!-- netstandard2.0 依赖 -->
1111
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
1212
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
13-
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.1.0" />
13+
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="3.1.6" />
1414
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
1515
</ItemGroup>
16-
16+
1717
<!-- netstandard2.1 依赖 -->
1818
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
1919
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.2" />
20-
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="3.1.2" />
20+
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="3.1.6" />
2121
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
2222
</ItemGroup>
2323

24-
<!-- .NET 5.0 依赖 -->
25-
<ItemGroup Condition=" '$(TargetFramework)' == 'net5.0' ">
26-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
27-
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="5.0.0" />
28-
</ItemGroup>
2924

3025
<!-- .NET 6.0+ 依赖 -->
3126
<ItemGroup Condition=" '$(TargetFramework)' == 'net6.0' ">

src/Magicodes.ExporterAndImporter.Csv/Utility/ImportHelper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ public Task<ImportResult<T>> Import(string filePath = null)
7171
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
7272
{
7373
csv.Context.RegisterClassMap<AutoMap<T>>();
74+
// Map empty CSV cells to null for all types (not empty string)
75+
csv.Context.TypeConverterOptionsCache.GetOptions<string>().NullValues.Add(string.Empty);
7476
var result = csv.GetRecords<T>();
7577
ImportResult.Data = result.ToList();
7678
return Task.FromResult(ImportResult);

src/Magicodes.ExporterAndImporter.Excel/Images/ImageExtensions.cs

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System.IO;
22
using System;
3-
using System.Net;
3+
using System.Net.Http;
44
using SkiaSharp;
55
using SixLabors.ImageSharp;
66
using Magicodes.IE.EPPlus;
@@ -9,6 +9,11 @@ namespace Magicodes.IE.Excel.Images
99
{
1010
public static partial class ImageExtensions
1111
{
12+
private static readonly HttpClient _httpClient = new HttpClient
13+
{
14+
Timeout = TimeSpan.FromSeconds(30)
15+
};
16+
1217
public struct ImageInfo
1318
{
1419
public int Width;
@@ -37,13 +42,7 @@ public static ImageInfo IdentifyImage(byte[] imageBytes)
3742

3843
public static byte[] DownloadImageBytes(this string url)
3944
{
40-
if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase))
41-
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
42-
using (var wc = new WebClient())
43-
{
44-
wc.Proxy = null;
45-
return wc.DownloadData(url);
46-
}
45+
return _httpClient.GetByteArrayAsync(url).ConfigureAwait(false).GetAwaiter().GetResult();
4746
}
4847

4948
public static byte[] ReadImageBytes(this string filePath)
@@ -74,29 +73,22 @@ private static string GetContentType(SKEncodedImageFormat format)
7473

7574
public static SixLabors.ImageSharp.Image GetImageByUrl(this string url, out SixLabors.ImageSharp.Formats.IImageFormat format)
7675
{
77-
if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase))
78-
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
79-
80-
using (var wc = new WebClient())
76+
using (Stream webStream = _httpClient.GetStreamAsync(url).ConfigureAwait(false).GetAwaiter().GetResult())
8177
{
82-
wc.Proxy = null;
83-
using (Stream webStream = wc.OpenRead(url))
78+
using (MemoryStream memoryStream = new MemoryStream())
8479
{
85-
using (MemoryStream memoryStream = new MemoryStream())
80+
webStream.CopyTo(memoryStream);
81+
memoryStream.Position = 0;
82+
83+
var image = SixLabors.ImageSharp.Image.Load(memoryStream);
84+
format = image.GetImageFormat(memoryStream);
85+
86+
if (image.Metadata.HorizontalResolution <= 1 && image.Metadata.VerticalResolution <= 1)
8687
{
87-
webStream.CopyTo(memoryStream);
88-
memoryStream.Position = 0;
89-
90-
var image = SixLabors.ImageSharp.Image.Load(memoryStream);
91-
format = image.GetImageFormat(memoryStream);
92-
93-
if (image.Metadata.HorizontalResolution <= 1 && image.Metadata.VerticalResolution <= 1)
94-
{
95-
image.Metadata.HorizontalResolution = SixLabors.ImageSharp.Metadata.ImageMetadata.DefaultHorizontalResolution;
96-
image.Metadata.VerticalResolution = SixLabors.ImageSharp.Metadata.ImageMetadata.DefaultVerticalResolution;
97-
}
98-
return image;
88+
image.Metadata.HorizontalResolution = SixLabors.ImageSharp.Metadata.ImageMetadata.DefaultHorizontalResolution;
89+
image.Metadata.VerticalResolution = SixLabors.ImageSharp.Metadata.ImageMetadata.DefaultVerticalResolution;
9990
}
91+
return image;
10092
}
10193
}
10294
}

src/Magicodes.ExporterAndImporter.Excel/Utility/ImportHelper.cs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ protected ExcelImporterAttribute ExcelImporterSettings
131131
/// </summary>
132132
private List<int> EmptyRows { get; } = new List<int>();
133133

134+
/// <summary>
135+
/// Data 索引 → 实际 Excel 行号映射(用于验证阶段正确报告行号)
136+
/// </summary>
137+
private List<int> DataRowIndices { get; } = new List<int>();
138+
134139
/// <summary>
135140
/// </summary>
136141
public void Dispose()
@@ -141,6 +146,7 @@ public void Dispose()
141146
ImportResult = null;
142147
Stream = null;
143148
dicMergePreValues = null;
149+
DataRowIndices.Clear();
144150
GC.Collect();
145151
}
146152

@@ -152,6 +158,7 @@ public Task<ImportResult<T>> Import(string filePath = null, Func<ImportResult<T>
152158
{
153159
if (!string.IsNullOrWhiteSpace(filePath)) FilePath = filePath;
154160
ImportResult = new ImportResult<T>();
161+
DataRowIndices.Clear();
155162
try
156163
{
157164
if (Stream == null)
@@ -188,7 +195,9 @@ public Task<ImportResult<T>> Import(string filePath = null, Func<ImportResult<T>
188195
out var validationResults);
189196
if (!isValid)
190197
{
191-
var rowIndex = ExcelImporterSettings.HeaderRowIndex + i + 1;
198+
var rowIndex = i < DataRowIndices.Count
199+
? DataRowIndices[i]
200+
: ExcelImporterSettings.HeaderRowIndex + i + 1;
192201
var dataRowError = GetDataRowErrorInfo(rowIndex);
193202
foreach (var validationResult in validationResults)
194203
{
@@ -1491,6 +1500,12 @@ protected virtual void ParseData(ExcelPackage excelPackage)
14911500
}
14921501
break;
14931502

1503+
case "DateOnly":
1504+
case "Nullable<DateOnly>":
1505+
HandleDateOnlyType(cell, dataItem, propertyInfo, rowIndex, col, cellValue,
1506+
propertyInfo.PropertyType.GetCSharpTypeName());
1507+
break;
1508+
14941509
case "TimeSpan":
14951510
{
14961511
if (cell.Value == null || cell.Text.IsNullOrWhiteSpace())
@@ -1628,6 +1643,7 @@ protected virtual void ParseData(ExcelPackage excelPackage)
16281643
}
16291644

16301645
ImportResult.Data.Add(dataItem);
1646+
DataRowIndices.Add(rowIndex);
16311647
}
16321648
}
16331649
}
@@ -1986,6 +2002,12 @@ protected virtual IEnumerable<T> ParseDataEnumer(ExcelPackage excelPackage)
19862002
}
19872003
break;
19882004

2005+
case "DateOnly":
2006+
case "Nullable<DateOnly>":
2007+
HandleDateOnlyType(cell, dataItem, propertyInfo, rowIndex, col, cellValue,
2008+
propertyInfo.PropertyType.GetCSharpTypeName());
2009+
break;
2010+
19892011
case "TimeSpan":
19902012
{
19912013
if (cell.Value == null || cell.Text.IsNullOrWhiteSpace())
@@ -2246,5 +2268,52 @@ public bool OutputBussinessErrorDataByte(Stream stream, List<DataRowErrorInfo> b
22462268
return false;
22472269
}
22482270
}
2271+
2272+
#if NET6_0_OR_GREATER
2273+
private void HandleDateOnlyType(ExcelRange cell, T dataItem, PropertyInfo propertyInfo,
2274+
int rowIndex, ImporterHeaderInfo col, string cellValue, string typeName)
2275+
{
2276+
if (typeName == "DateOnly")
2277+
{
2278+
if (cell.Value == null || cell.Text.IsNullOrWhiteSpace())
2279+
{
2280+
AddRowDataError(rowIndex, col, $"{Resource.Value} {cell.Value} {Resource.PleaseFillInTheCorrectDateAndTimeFormat}");
2281+
return;
2282+
}
2283+
try
2284+
{
2285+
var date = cell.GetValue<DateTime>();
2286+
SetValue(cell, dataItem, propertyInfo, DateOnly.FromDateTime(date));
2287+
}
2288+
catch (Exception)
2289+
{
2290+
AddRowDataError(rowIndex, col, $"{Resource.Value} {cell.Value} {Resource.PleaseFillInTheCorrectDateAndTimeFormat}");
2291+
}
2292+
}
2293+
else // Nullable<DateOnly>
2294+
{
2295+
if (string.IsNullOrWhiteSpace(cell.Text))
2296+
{
2297+
SetValue(cell, dataItem, propertyInfo, null);
2298+
return;
2299+
}
2300+
try
2301+
{
2302+
var date = cell.GetValue<DateTime>();
2303+
SetValue(cell, dataItem, propertyInfo, (DateOnly?)DateOnly.FromDateTime(date));
2304+
}
2305+
catch (Exception)
2306+
{
2307+
AddRowDataError(rowIndex, col, $"{Resource.Value} {cell.Value} {Resource.PleaseFillInTheCorrectDateAndTimeFormat}");
2308+
}
2309+
}
2310+
}
2311+
#else
2312+
private void HandleDateOnlyType(ExcelRange cell, T dataItem, PropertyInfo propertyInfo,
2313+
int rowIndex, ImporterHeaderInfo col, string cellValue, string typeName)
2314+
{
2315+
AddRowDataError(rowIndex, col, "DateOnly is not supported on this target framework.");
2316+
}
2317+
#endif
22492318
}
22502319
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#if NET6_0_OR_GREATER
2+
using Magicodes.ExporterAndImporter.Core;
3+
using Magicodes.ExporterAndImporter.Excel;
4+
using Magicodes.ExporterAndImporter.Tests.Models.Import;
5+
using Newtonsoft.Json;
6+
using OfficeOpenXml;
7+
using Shouldly;
8+
using System.IO;
9+
using System.Threading.Tasks;
10+
using Xunit;
11+
using Xunit.Abstractions;
12+
13+
namespace Magicodes.ExporterAndImporter.Tests
14+
{
15+
/// <summary>
16+
/// #582: 模板 Excel 有空行时,错误提示行号偏移。
17+
/// </summary>
18+
public class Issue582_EmptyRowTemplateImport_Tests : TestBase
19+
{
20+
private readonly ITestOutputHelper _output;
21+
22+
public Issue582_EmptyRowTemplateImport_Tests(ITestOutputHelper output)
23+
{
24+
_output = output;
25+
}
26+
27+
#region 测试数据准备
28+
29+
/// <summary>
30+
/// Row 1: 姓名, 年龄, 城市
31+
/// Row 2: 张三, 25, 北京
32+
/// Row 3: (空行)
33+
/// Row 4: (姓名空), 30, 上海
34+
/// </summary>
35+
private string CreateTemplateWithEmptyRow()
36+
{
37+
var path = GetTestFilePath($"Issue582_EmptyRow_{System.Guid.NewGuid():N}.xlsx");
38+
using (var pck = new ExcelPackage())
39+
{
40+
var ws = pck.Workbook.Worksheets.Add("Sheet1");
41+
ws.Cells[1, 1].Value = "姓名";
42+
ws.Cells[1, 2].Value = "年龄";
43+
ws.Cells[1, 3].Value = "城市";
44+
45+
ws.Cells[2, 1].Value = "张三";
46+
ws.Cells[2, 2].Value = 25;
47+
ws.Cells[2, 3].Value = "北京";
48+
49+
// Row 3 intentionally empty
50+
51+
// Row 4: Name is empty (will fail IsRequired)
52+
ws.Cells[4, 2].Value = 30;
53+
ws.Cells[4, 3].Value = "上海";
54+
55+
pck.SaveAs(new FileInfo(path));
56+
}
57+
return path;
58+
}
59+
60+
#endregion
61+
62+
#region 空行模板导入
63+
64+
[Fact(DisplayName = "#582 模板有空行:错误行号应反映实际行")]
65+
public async Task EmptyRowTemplateImport_ErrorRowIndex_ShouldMatchActualRow()
66+
{
67+
var filePath = CreateTemplateWithEmptyRow();
68+
try
69+
{
70+
IExcelImporter importer = new ExcelImporter();
71+
var import = await importer.Import<EmptyRowTemplateImportDto>(filePath);
72+
73+
import.ShouldNotBeNull();
74+
// Row 3 is empty → skipped. Row 2 (valid) + Row 4 (invalid, missing Name) are both added to Data.
75+
// The importer adds all non-empty rows to Data regardless of validation errors,
76+
// with errors tracked separately in RowErrors.
77+
import.Data.Count.ShouldBe(2);
78+
79+
// Row 4 has no Name → should report error at Row 4, not Row 3
80+
import.RowErrors.Count.ShouldBe(1);
81+
82+
foreach (var rowError in import.RowErrors)
83+
{
84+
_output.WriteLine($"RowError at index {rowError.RowIndex}: {JsonConvert.SerializeObject(rowError.FieldErrors)}");
85+
}
86+
87+
// The key assertion: Row 4 has IsRequired violation on "姓名"
88+
// RowIndex should be 4 (actual row), not 3 (offset due to empty row skip)
89+
import.RowErrors.ShouldContain(e =>
90+
e.FieldErrors.ContainsKey("姓名") &&
91+
e.RowIndex == 4);
92+
}
93+
finally
94+
{
95+
DeleteFile(filePath);
96+
}
97+
}
98+
99+
#endregion
100+
}
101+
}
102+
#endif

0 commit comments

Comments
 (0)