Compare commits

..

2 Commits

Author SHA1 Message Date
zzh
9e5d6d20d5 feat: 会员概览补充等级编码与图标,收发件人国家返回二字码
All checks were successful
Build and Deploy / deploy (push) Successful in 59s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 17:34:32 +08:00
zzh
ae4e733a71 feat: KPI 分区导入支持增量/全量,删除分区级联明细
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 17:34:31 +08:00
9 changed files with 93 additions and 13 deletions

View File

@ -12,8 +12,19 @@ public class KpiAreaInfoImportInput
/// </summary>
public int AreaId { get; set; }
/// <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";
}
}

View File

@ -4,8 +4,12 @@ public class MemberOverviewOutput
{
public int? MemberLevelId { get; set; }
public string? MemberLevelCode { get; set; }
public string? MemberLevelName { get; set; }
public string? IconUrl { get; set; }
public string? PriceTierKey { get; set; }
public int TotalPoints { get; set; }

View File

@ -31,9 +31,9 @@ public class OrderReceiverOutput
public string? Email { get; set; }
/// <summary>
/// 国家/地区代码。
/// 国家/地区二字码。
/// </summary>
public int? CountryCode { get; set; }
public string? CountryCode { get; set; }
/// <summary>
/// 省份。

View File

@ -31,9 +31,9 @@ public class OrderSenderOutput
public string? Email { get; set; }
/// <summary>
/// 国家/地区代码。
/// 国家/地区二字码。
/// </summary>
public int? CountryCode { get; set; }
public string? CountryCode { get; set; }
/// <summary>
/// 省份。

View File

@ -91,6 +91,7 @@ public class KpiAreaService : ApplicationService
public async Task DeleteAsync(IdInput input)
{
var entity = await GetAreaOrThrowAsync(input.Id);
await KpiAreaRepository.DeleteInfosByAreaIdAsync(entity.Id);
await KpiAreaRepository.DeleteAsync(entity);
}
@ -203,6 +204,7 @@ public class KpiAreaService : ApplicationService
}
await GetAreaOrThrowAsync(input.AreaId);
var importMode = NormalizeImportMode(input.ImportMode);
await using var stream = input.File.OpenReadStream();
var parsedRows = await KpiAreaInfoImportHelper.ParseImportRowsAsync(stream);
@ -218,9 +220,16 @@ public class KpiAreaService : ApplicationService
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
var existingKeys = new HashSet<string>(StringComparer.Ordinal);
foreach (var item in await KpiAreaRepository.GetAllInfosByAreaIdAsync(input.AreaId))
if (importMode == KpiAreaInfoImportInput.ImportModes.Full)
{
TryAddDuplicateKey(existingKeys, item);
await KpiAreaRepository.DeleteInfosByAreaIdAsync(input.AreaId);
}
else
{
foreach (var item in await KpiAreaRepository.GetAllInfosByAreaIdAsync(input.AreaId))
{
TryAddDuplicateKey(existingKeys, item);
}
}
var output = new KpiAreaInfoImportOutput();
@ -236,7 +245,9 @@ public class KpiAreaService : ApplicationService
output.Failures.Add(new KpiAreaInfoImportFailRow
{
RowNumber = rowNumber,
Reason = "与已有规则重复,已跳过"
Reason = importMode == KpiAreaInfoImportInput.ImportModes.Full
? "文件内规则重复,已跳过"
: "与已有规则重复,已跳过"
});
continue;
}
@ -258,6 +269,18 @@ public class KpiAreaService : ApplicationService
return output;
}
private static string NormalizeImportMode(string? mode)
{
if (string.Equals(mode, KpiAreaInfoImportInput.ImportModes.Full, StringComparison.OrdinalIgnoreCase)
|| string.Equals(mode, "全量", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mode, "replace", StringComparison.OrdinalIgnoreCase))
{
return KpiAreaInfoImportInput.ImportModes.Full;
}
return KpiAreaInfoImportInput.ImportModes.Incremental;
}
private static Expression<Func<KpiArea, bool>> BuildAreaPredicate(KpiAreaListInput input)
{
var appKey = input.AppKey?.Trim();

View File

@ -57,7 +57,9 @@ public class MarketingPortalMemberService : ApplicationService
return new MemberOverviewOutput
{
MemberLevelId = currentLevel?.Id,
MemberLevelCode = currentLevel?.LevelCode,
MemberLevelName = currentLevel?.LevelName,
IconUrl = currentLevel?.IconUrl,
PriceTierKey = MarketingCustomerHelper.NormalizePriceTierKeyOrDefault(mainCustomer.PriceTierKey),
TotalPoints = account.TotalPoints,
RollingPoints = rollingPoints,

View File

@ -79,6 +79,20 @@ public partial class OrderService
: await CountryRepository.GetListAsync(x => countryCodes.Contains(x.CountryCode));
var countryMap = countries.ToDictionary(x => x.CountryCode, StringComparer.OrdinalIgnoreCase);
var addressCountryIds = senders
.Select(x => x.Country)
.Concat(recipients.Select(x => x.Country))
.Where(x => x.HasValue)
.Select(x => x!.Value)
.Distinct()
.ToArray();
var addressCountries = addressCountryIds.Length == 0
? []
: await CountryRepository.GetListAsync(x => addressCountryIds.Contains(x.Id));
var countryIdCodeMap = addressCountries.ToDictionary(
x => x.Id,
x => x.CountryCode ?? string.Empty);
var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(
orders.SelectMany(x => new[] { x.CreateUserId, x.UpdateUserId }));
@ -252,8 +266,8 @@ public partial class OrderService
CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(order.CreateUserId, auditUserNameMap),
UpdateTime = OmsDocHelper.NullIfDefault(order.UpdateTime),
UpdateBy = OmsRbacUserDisplayNameResolver.FormatUserId(order.UpdateUserId, auditUserNameMap),
Sender = MapSender(sender),
Receiver = MapReceiver(recipient),
Sender = MapSender(sender, countryIdCodeMap),
Receiver = MapReceiver(recipient, countryIdCodeMap),
Items = (items ?? []).Select(MapDeclaration).ToList(),
WeightList = (orderWeights ?? []).Select(MapWeight).ToList(),
FeeList = feeOutputs,
@ -705,7 +719,9 @@ public partial class OrderService
: null;
}
private static OrderSenderOutput? MapSender(OrderSender? sender)
private static OrderSenderOutput? MapSender(
OrderSender? sender,
IReadOnlyDictionary<int, string> countryIdCodeMap)
{
return sender == null
? null
@ -716,7 +732,7 @@ public partial class OrderService
Phone = sender.Phone,
TelePhone = sender.TelePhone,
Email = sender.Email,
CountryCode = sender.Country,
CountryCode = ResolveCountryCode(sender.Country, countryIdCodeMap),
Province = sender.Province,
City = sender.City,
ZipCode = sender.ZipCode,
@ -727,7 +743,9 @@ public partial class OrderService
};
}
private static OrderReceiverOutput? MapReceiver(OrderRecipient? recipient)
private static OrderReceiverOutput? MapReceiver(
OrderRecipient? recipient,
IReadOnlyDictionary<int, string> countryIdCodeMap)
{
return recipient == null
? null
@ -738,7 +756,7 @@ public partial class OrderService
Phone = recipient.Phone,
TelePhone = recipient.TelePhone,
Email = recipient.Email,
CountryCode = recipient.Country,
CountryCode = ResolveCountryCode(recipient.Country, countryIdCodeMap),
Province = recipient.Province,
City = recipient.City,
StreetNumber = recipient.StreetNumber,
@ -755,6 +773,20 @@ public partial class OrderService
};
}
private static string? ResolveCountryCode(
int? countryId,
IReadOnlyDictionary<int, string> countryIdCodeMap)
{
if (!countryId.HasValue)
{
return null;
}
return countryIdCodeMap.TryGetValue(countryId.Value, out var code) && !string.IsNullOrWhiteSpace(code)
? code
: null;
}
private OrderItemOutput MapDeclaration(OrderDeclaration x)
{
return new OrderItemOutput

View File

@ -21,5 +21,7 @@ public interface IKpiAreaRepository : IRepository<KpiArea>, IKpiAreaPublishRepos
Task DeleteInfoAsync(int id, CancellationToken cancellationToken = default);
Task DeleteInfosByAreaIdAsync(int areaId, CancellationToken cancellationToken = default);
Task<List<KpiAreaInfo>> GetAllInfosByAreaIdAsync(int areaId, CancellationToken cancellationToken = default);
}

View File

@ -57,6 +57,12 @@ public class KpiAreaRepository : OmsSqlSugarRepository<KpiArea>, IKpiAreaReposit
await db.Deleteable<KpiAreaInfo>().Where(x => x.Id == id).ExecuteCommandAsync(cancellationToken);
}
public async Task DeleteInfosByAreaIdAsync(int areaId, CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
await db.Deleteable<KpiAreaInfo>().Where(x => x.AreaId == areaId).ExecuteCommandAsync(cancellationToken);
}
public async Task<List<KpiAreaInfo>> GetAllInfosByAreaIdAsync(int areaId, CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);