feat: 国家地区支持增删改与导入,列表返回创建和更新时间
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5afd204d36
commit
8f64bbf02d
25
src/Oms.Application/Dtos/System/CountryImportInput.cs
Normal file
25
src/Oms.Application/Dtos/System/CountryImportInput.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Oms.Application.Dtos.System;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 国家/地区 Excel 导入入参。
|
||||||
|
/// </summary>
|
||||||
|
public class CountryImportInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 导入模式:incremental=按二字码新增或更新;full=按二字码新增或更新,并软删除文件中未出现的国家。
|
||||||
|
/// </summary>
|
||||||
|
public string ImportMode { get; set; } = ImportModes.Incremental;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Excel 文件。
|
||||||
|
/// </summary>
|
||||||
|
public IFormFile File { get; set; } = null!;
|
||||||
|
|
||||||
|
public static class ImportModes
|
||||||
|
{
|
||||||
|
public const string Incremental = "incremental";
|
||||||
|
public const string Full = "full";
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/Oms.Application/Dtos/System/CountryImportOutput.cs
Normal file
48
src/Oms.Application/Dtos/System/CountryImportOutput.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
namespace Oms.Application.Dtos.System;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 国家/地区 Excel 导入结果。
|
||||||
|
/// </summary>
|
||||||
|
public class CountryImportOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 成功写入条数。
|
||||||
|
/// </summary>
|
||||||
|
public int SuccessCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 跳过条数。
|
||||||
|
/// </summary>
|
||||||
|
public int SkipCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 失败条数。
|
||||||
|
/// </summary>
|
||||||
|
public int FailCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全量导入时软删除的条数。
|
||||||
|
/// </summary>
|
||||||
|
public int DeletedCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 失败行明细。
|
||||||
|
/// </summary>
|
||||||
|
public List<CountryImportFailRow> Failures { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导入失败行。
|
||||||
|
/// </summary>
|
||||||
|
public class CountryImportFailRow
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Excel 行号(含表头,从 2 起为数据行)。
|
||||||
|
/// </summary>
|
||||||
|
public int RowNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 失败原因。
|
||||||
|
/// </summary>
|
||||||
|
public string Reason { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@ -24,4 +24,14 @@ public class CountryOutput
|
|||||||
/// ISO 3166-1 alpha-2 国家二字码。
|
/// ISO 3166-1 alpha-2 国家二字码。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string CountryCode { get; set; } = string.Empty;
|
public string CountryCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreateTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新时间;未更新时为空。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? UpdateTime { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/Oms.Application/Dtos/System/CreateCountryInput.cs
Normal file
27
src/Oms.Application/Dtos/System/CreateCountryInput.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Oms.Application.Dtos.System;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增国家/地区入参。
|
||||||
|
/// </summary>
|
||||||
|
public class CreateCountryInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 国家中文名。
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string ChineseName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 国家英文名。
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string EnglishName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ISO 3166-1 alpha-2 国家二字码。
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string CountryCode { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
33
src/Oms.Application/Dtos/System/UpdateCountryInput.cs
Normal file
33
src/Oms.Application/Dtos/System/UpdateCountryInput.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Oms.Application.Dtos.System;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新国家/地区入参。
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateCountryInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 主键。
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 国家中文名。
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string ChineseName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 国家英文名。
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string EnglishName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ISO 3166-1 alpha-2 国家二字码。
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string CountryCode { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@ -1,6 +1,9 @@
|
|||||||
using Fake.Application;
|
using Fake.Application;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Oms.Application.Dtos.Shared;
|
||||||
using Oms.Application.Dtos.System;
|
using Oms.Application.Dtos.System;
|
||||||
|
using Oms.Application.Shared;
|
||||||
|
using Oms.Domain.SystemAggregate.Entities;
|
||||||
using Oms.Domain.SystemAggregate.Repositories;
|
using Oms.Domain.SystemAggregate.Repositories;
|
||||||
|
|
||||||
namespace Oms.Application.Services.System;
|
namespace Oms.Application.Services.System;
|
||||||
@ -16,20 +19,249 @@ public class CountryService : ApplicationService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询国家列表。
|
/// 查询国家列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>国家列表,返回全部有效国家主数据。</returns>
|
/// <returns>国家列表,返回全部有效国家主数据,按二字码、英文名排序。</returns>
|
||||||
[HttpGet("/system/country/list")]
|
[HttpGet("/system/country/list")]
|
||||||
public async Task<List<CountryOutput>> GetListAsync()
|
public async Task<List<CountryOutput>> GetListAsync()
|
||||||
{
|
{
|
||||||
return (await CountryRepository.GetListAsync())
|
return (await CountryRepository.GetListAsync())
|
||||||
.OrderBy(x => x.CountryCode)
|
.OrderBy(x => x.CountryCode)
|
||||||
.ThenBy(x => x.EnglishName)
|
.ThenBy(x => x.EnglishName)
|
||||||
.Select(x => new CountryOutput
|
.Select(ToOutput)
|
||||||
{
|
|
||||||
Id = x.Id,
|
|
||||||
ChineseName = x.ChineseName,
|
|
||||||
EnglishName = x.EnglishName,
|
|
||||||
CountryCode = x.CountryCode
|
|
||||||
})
|
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增国家/地区。二字码已存在时失败;若仅存在同码软删除记录则恢复该记录。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">中文名、英文名、二字码。</param>
|
||||||
|
/// <returns>无返回体,仅完成新增。</returns>
|
||||||
|
[HttpPost("/system/country/create")]
|
||||||
|
public async Task CreateAsync(CreateCountryInput input)
|
||||||
|
{
|
||||||
|
var row = ToImportRow(input.ChineseName, input.EnglishName, input.CountryCode);
|
||||||
|
await SaveAsync(row, excludeId: null, updateWhenCodeExists: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新国家/地区。二字码与其他有效记录冲突时失败。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">主键及中文名、英文名、二字码。</param>
|
||||||
|
/// <returns>无返回体,仅完成更新。</returns>
|
||||||
|
[HttpPost("/system/country/update")]
|
||||||
|
public async Task UpdateAsync(UpdateCountryInput input)
|
||||||
|
{
|
||||||
|
var entity = await GetOrThrowAsync(input.Id);
|
||||||
|
var row = ToImportRow(input.ChineseName, input.EnglishName, input.CountryCode);
|
||||||
|
await SaveAsync(row, excludeId: entity.Id, updateWhenCodeExists: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除国家/地区(软删除)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">目标主键。</param>
|
||||||
|
/// <returns>无返回体,仅完成删除。</returns>
|
||||||
|
[HttpPost("/system/country/delete")]
|
||||||
|
public async Task DeleteAsync(IdInput input)
|
||||||
|
{
|
||||||
|
var entity = await GetOrThrowAsync(input.Id);
|
||||||
|
await CountryRepository.DeleteAsync(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载国家/地区导入模板。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>xlsx 文件,列为国家二字码、中文名、英文名。</returns>
|
||||||
|
[HttpGet("/system/country/import-template")]
|
||||||
|
public async Task<FileContentResult> DownloadImportTemplateAsync()
|
||||||
|
{
|
||||||
|
var bytes = await CountryImportHelper.BuildTemplateBytesAsync();
|
||||||
|
return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
{
|
||||||
|
FileDownloadName = CountryImportHelper.TemplateFileName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导入国家/地区(Excel)。增量按二字码新增或更新;全量还会软删除文件中未出现的国家。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">Excel 文件与导入模式。</param>
|
||||||
|
/// <returns>成功、跳过、失败及全量删除条数。</returns>
|
||||||
|
[HttpPost("/system/country/import")]
|
||||||
|
public async Task<CountryImportOutput> ImportAsync([FromForm] CountryImportInput input)
|
||||||
|
{
|
||||||
|
if (input.File == null || input.File.Length == 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("请上传 Excel 文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
var importMode = NormalizeImportMode(input.ImportMode);
|
||||||
|
await using var stream = input.File.OpenReadStream();
|
||||||
|
var parsedRows = await CountryImportHelper.ParseImportRowsAsync(stream);
|
||||||
|
if (parsedRows.Count == 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("Excel 中无有效数据行");
|
||||||
|
}
|
||||||
|
|
||||||
|
var output = new CountryImportOutput();
|
||||||
|
var touchedCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var seenCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var (rowNumber, row) in parsedRows)
|
||||||
|
{
|
||||||
|
var rawCode = row.CountryCode?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(rawCode))
|
||||||
|
{
|
||||||
|
touchedCodes.Add(rawCode.ToUpperInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CountryImportHelper.Normalize(row);
|
||||||
|
var code = row.CountryCode ?? string.Empty;
|
||||||
|
if (!seenCodes.Add(code))
|
||||||
|
{
|
||||||
|
output.SkipCount++;
|
||||||
|
output.Failures.Add(new CountryImportFailRow
|
||||||
|
{
|
||||||
|
RowNumber = rowNumber,
|
||||||
|
Reason = "文件内二字码重复,已跳过"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SaveAsync(row, excludeId: null, updateWhenCodeExists: true);
|
||||||
|
output.SuccessCount++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
output.FailCount++;
|
||||||
|
output.Failures.Add(new CountryImportFailRow
|
||||||
|
{
|
||||||
|
RowNumber = rowNumber,
|
||||||
|
Reason = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importMode == CountryImportInput.ImportModes.Full && output.SuccessCount > 0)
|
||||||
|
{
|
||||||
|
var existing = await CountryRepository.GetListAsync();
|
||||||
|
foreach (var entity in existing)
|
||||||
|
{
|
||||||
|
var code = entity.CountryCode?.Trim().ToUpperInvariant() ?? string.Empty;
|
||||||
|
if (touchedCodes.Contains(code))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CountryRepository.DeleteAsync(entity);
|
||||||
|
output.DeletedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveAsync(CountryImportRow row, int? excludeId, bool updateWhenCodeExists)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CountryImportHelper.Normalize(row);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
var active = await CountryRepository.GetByCountryCodeAsync(row.CountryCode);
|
||||||
|
if (active != null && active.Id != excludeId)
|
||||||
|
{
|
||||||
|
if (updateWhenCodeExists)
|
||||||
|
{
|
||||||
|
ApplyNames(active, row);
|
||||||
|
await CountryRepository.UpdateAsync(active);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BusinessException("国家二字码已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleted = await CountryRepository.GetByCountryCodeIncludingDeletedAsync(row.CountryCode);
|
||||||
|
if (deleted != null && deleted.IsDeleted && deleted.Id != excludeId)
|
||||||
|
{
|
||||||
|
if (excludeId != null)
|
||||||
|
{
|
||||||
|
throw new BusinessException("国家二字码已被删除记录占用,请新增同码以恢复");
|
||||||
|
}
|
||||||
|
|
||||||
|
deleted.IsDeleted = false;
|
||||||
|
deleted.CountryCode = row.CountryCode;
|
||||||
|
ApplyNames(deleted, row);
|
||||||
|
await CountryRepository.UpdateAsync(deleted);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeId != null)
|
||||||
|
{
|
||||||
|
var entity = await GetOrThrowAsync(excludeId.Value);
|
||||||
|
entity.CountryCode = row.CountryCode;
|
||||||
|
ApplyNames(entity, row);
|
||||||
|
await CountryRepository.UpdateAsync(entity);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CountryRepository.InsertAsync(new Country
|
||||||
|
{
|
||||||
|
CountryCode = row.CountryCode,
|
||||||
|
ChineseName = row.ChineseName,
|
||||||
|
EnglishName = row.EnglishName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyNames(Country entity, CountryImportRow row)
|
||||||
|
{
|
||||||
|
entity.ChineseName = row.ChineseName;
|
||||||
|
entity.EnglishName = row.EnglishName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CountryImportRow ToImportRow(string chineseName, string englishName, string countryCode) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ChineseName = chineseName,
|
||||||
|
EnglishName = englishName,
|
||||||
|
CountryCode = countryCode
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task<Country> GetOrThrowAsync(int id)
|
||||||
|
{
|
||||||
|
var entity = await CountryRepository.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
if (entity == null)
|
||||||
|
{
|
||||||
|
throw new BusinessException("国家不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CountryOutput ToOutput(Country x) => new()
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
ChineseName = x.ChineseName,
|
||||||
|
EnglishName = x.EnglishName,
|
||||||
|
CountryCode = x.CountryCode,
|
||||||
|
CreateTime = x.CreateTime,
|
||||||
|
UpdateTime = x.UpdateTime == default ? null : x.UpdateTime
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string NormalizeImportMode(string? mode)
|
||||||
|
{
|
||||||
|
if (string.Equals(mode, CountryImportInput.ImportModes.Full, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(mode, "全量", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(mode, "replace", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return CountryImportInput.ImportModes.Full;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CountryImportInput.ImportModes.Incremental;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
179
src/Oms.Application/Shared/CountryImportHelper.cs
Normal file
179
src/Oms.Application/Shared/CountryImportHelper.cs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using MiniExcelLibs;
|
||||||
|
|
||||||
|
namespace Oms.Application.Shared;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 国家/地区 Excel 模板与导入解析。
|
||||||
|
/// </summary>
|
||||||
|
public static class CountryImportHelper
|
||||||
|
{
|
||||||
|
public const string TemplateSheetName = "导入模板";
|
||||||
|
public const string TemplateFileName = "国家地区导入模板.xlsx";
|
||||||
|
|
||||||
|
private static readonly Regex CountryCodePattern = new("^[A-Za-z]{2}$", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, string> HeaderAliases = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["国家二字码"] = nameof(CountryImportRow.CountryCode),
|
||||||
|
["二字码"] = nameof(CountryImportRow.CountryCode),
|
||||||
|
["国家代码"] = nameof(CountryImportRow.CountryCode),
|
||||||
|
["CountryCode"] = nameof(CountryImportRow.CountryCode),
|
||||||
|
["countryCode"] = nameof(CountryImportRow.CountryCode),
|
||||||
|
["中文名"] = nameof(CountryImportRow.ChineseName),
|
||||||
|
["国家中文名"] = nameof(CountryImportRow.ChineseName),
|
||||||
|
["ChineseName"] = nameof(CountryImportRow.ChineseName),
|
||||||
|
["chineseName"] = nameof(CountryImportRow.ChineseName),
|
||||||
|
["英文名"] = nameof(CountryImportRow.EnglishName),
|
||||||
|
["国家英文名"] = nameof(CountryImportRow.EnglishName),
|
||||||
|
["EnglishName"] = nameof(CountryImportRow.EnglishName),
|
||||||
|
["englishName"] = nameof(CountryImportRow.EnglishName),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task<byte[]> BuildTemplateBytesAsync()
|
||||||
|
{
|
||||||
|
var templateRows = new List<Dictionary<string, object?>>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
["国家二字码"] = "CN",
|
||||||
|
["中文名"] = "中国",
|
||||||
|
["英文名"] = "China"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
["国家二字码"] = "US",
|
||||||
|
["中文名"] = "美国",
|
||||||
|
["英文名"] = "United States"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var sheets = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[TemplateSheetName] = templateRows
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var stream = new MemoryStream();
|
||||||
|
await MiniExcel.SaveAsAsync(stream, sheets);
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<List<(int RowNumber, CountryImportRow Row)>> ParseImportRowsAsync(Stream fileStream)
|
||||||
|
{
|
||||||
|
if (fileStream.CanSeek)
|
||||||
|
{
|
||||||
|
fileStream.Position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sheetNames = MiniExcel.GetSheetNames(fileStream);
|
||||||
|
var sheetName = sheetNames.FirstOrDefault(x =>
|
||||||
|
string.Equals(x, TemplateSheetName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? sheetNames.FirstOrDefault();
|
||||||
|
|
||||||
|
if (fileStream.CanSeek)
|
||||||
|
{
|
||||||
|
fileStream.Position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = await MiniExcel.QueryAsync(fileStream, useHeaderRow: true, sheetName: sheetName);
|
||||||
|
var result = new List<(int RowNumber, CountryImportRow Row)>();
|
||||||
|
var rowNumber = 1;
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
rowNumber++;
|
||||||
|
if (row is not IDictionary<string, object> rowDict || IsEmptyRow(rowDict))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add((rowNumber, ParseRow(rowDict)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Normalize(CountryImportRow row)
|
||||||
|
{
|
||||||
|
row.CountryCode = row.CountryCode?.Trim().ToUpperInvariant() ?? string.Empty;
|
||||||
|
row.ChineseName = row.ChineseName?.Trim() ?? string.Empty;
|
||||||
|
row.EnglishName = row.EnglishName?.Trim() ?? string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(row.CountryCode))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("国家二字码不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CountryCodePattern.IsMatch(row.CountryCode))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("国家二字码须为 2 位字母,如 CN、US");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(row.ChineseName))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("中文名不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.ChineseName.Length > 128)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("中文名不可超过 128 个字符");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(row.EnglishName))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("英文名不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.EnglishName.Length > 128)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("英文名不可超过 128 个字符");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CountryImportRow ParseRow(IDictionary<string, object> rowDict)
|
||||||
|
{
|
||||||
|
var mapped = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var (header, value) in rowDict)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(header) || !HeaderAliases.TryGetValue(header.Trim(), out var field))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapped[field] = NormalizeCell(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CountryImportRow
|
||||||
|
{
|
||||||
|
CountryCode = mapped.GetValueOrDefault(nameof(CountryImportRow.CountryCode)) ?? string.Empty,
|
||||||
|
ChineseName = mapped.GetValueOrDefault(nameof(CountryImportRow.ChineseName)) ?? string.Empty,
|
||||||
|
EnglishName = mapped.GetValueOrDefault(nameof(CountryImportRow.EnglishName)) ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeCell(object? value)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)?.Trim();
|
||||||
|
return string.IsNullOrWhiteSpace(text) ? null : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsEmptyRow(IDictionary<string, object> rowDict) =>
|
||||||
|
rowDict.Values.All(value => string.IsNullOrWhiteSpace(NormalizeCell(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 国家导入行。
|
||||||
|
/// </summary>
|
||||||
|
public class CountryImportRow
|
||||||
|
{
|
||||||
|
public string CountryCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string ChineseName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string EnglishName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@ -6,4 +6,9 @@ namespace Oms.Domain.SystemAggregate.Repositories;
|
|||||||
public interface ICountryRepository : IRepository<Country>
|
public interface ICountryRepository : IRepository<Country>
|
||||||
{
|
{
|
||||||
Task<Country?> GetByCountryCodeAsync(string countryCode, CancellationToken cancellationToken = default);
|
Task<Country?> GetByCountryCodeAsync(string countryCode, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按二字码查询,包含已软删除记录,用于恢复同码数据。
|
||||||
|
/// </summary>
|
||||||
|
Task<Country?> GetByCountryCodeIncludingDeletedAsync(string countryCode, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,4 +11,15 @@ public class CountryRepository : OmsSqlSugarRepository<Country>, ICountryReposit
|
|||||||
{
|
{
|
||||||
return FirstOrDefaultAsync(x => x.CountryCode == countryCode, cancellationToken);
|
return FirstOrDefaultAsync(x => x.CountryCode == countryCode, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Country?> GetByCountryCodeIncludingDeletedAsync(
|
||||||
|
string countryCode,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
|
return await SoftDeleteQuery.FirstIncludingDeletedAsync<Country>(
|
||||||
|
db,
|
||||||
|
x => x.CountryCode == countryCode,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user