diff --git a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs
index 344f1ab..5cf568d 100644
--- a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs
+++ b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs
@@ -388,10 +388,11 @@ public partial class SalesProductService
};
///
- /// 保险/偏远费等附加费用项的计费条件校验。
+ /// 保险/偏远费等附加费用项的计费条件校验;同一提交批次内费用项不可重复。
///
private static void ValidateBillingPriceItemRules(IReadOnlyList prices)
{
+ EnsureUniqueCostItemsInBatch(prices.Select(x => x.CostItem));
foreach (var price in prices)
{
var costItem = price.CostItem?.Trim() ?? string.Empty;
@@ -417,6 +418,44 @@ public partial class SalesProductService
}
}
+ ///
+ /// 同一销售产品下每种费用项只能有一条卖价配置。
+ ///
+ private async Task EnsureCostItemUniqueAsync(int salesProductId, string? costItem, int? excludePriceId)
+ {
+ var trimmed = costItem?.Trim() ?? string.Empty;
+ if (string.IsNullOrWhiteSpace(trimmed))
+ {
+ throw new Fake.Application.BusinessException("费用项不能为空");
+ }
+
+ var existing = await SalesProductRepository.GetPricesAsync(salesProductId);
+ var duplicated = existing.Any(p =>
+ (!excludePriceId.HasValue || p.Id != excludePriceId.Value)
+ && string.Equals(p.CostItem?.Trim(), trimmed, StringComparison.OrdinalIgnoreCase));
+ if (duplicated)
+ {
+ throw new Fake.Application.BusinessException(
+ $"该产品已存在费用项「{trimmed}」,同一产品下每种费用项只能有一条");
+ }
+ }
+
+ private static void EnsureUniqueCostItemsInBatch(IEnumerable costItems)
+ {
+ var duplicates = costItems
+ .Select(x => x?.Trim() ?? string.Empty)
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .GroupBy(x => x, StringComparer.OrdinalIgnoreCase)
+ .Where(g => g.Count() > 1)
+ .Select(g => g.Key)
+ .ToList();
+ if (duplicates.Count > 0)
+ {
+ throw new Fake.Application.BusinessException(
+ $"同一产品下费用项不能重复:{string.Join("、", duplicates)}");
+ }
+ }
+
private static string? NormalizeAdditionalServices(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
diff --git a/src/Oms.Application/Services/Channels/SalesProductService.cs b/src/Oms.Application/Services/Channels/SalesProductService.cs
index 3b86f15..29ddaf2 100644
--- a/src/Oms.Application/Services/Channels/SalesProductService.cs
+++ b/src/Oms.Application/Services/Channels/SalesProductService.cs
@@ -159,6 +159,7 @@ public partial class SalesProductService : ApplicationService
{
await GetProductOrThrowAsync(input.SalesProductId);
ValidateBillingPriceItemRules([input]);
+ await EnsureCostItemUniqueAsync(input.SalesProductId, input.CostItem, excludePriceId: null);
await SalesProductRepository.InsertPriceAsync(
new SalesProductPrice
{
@@ -190,6 +191,7 @@ public partial class SalesProductService : ApplicationService
}
ValidateBillingPriceItemRules([input]);
+ await EnsureCostItemUniqueAsync(input.SalesProductId, input.CostItem, excludePriceId: price.Id);
price.SerialNumber = input.SerialNumber;
price.Status = input.Status;
price.PriceName = input.PriceName;
diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs
index 2a00598..0f8c182 100644
--- a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs
+++ b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs
@@ -329,10 +329,11 @@ public partial class ServiceChannelService
}
///
- /// 买价偏远费等附加费用项的计费条件校验。
+ /// 买价偏远费等附加费用项的计费条件校验;同一提交批次内费用项不可重复。
///
private static void ValidateBillingPriceItemRules(IReadOnlyList prices)
{
+ EnsureUniqueCostItemsInBatch(prices.Select(x => x.CostItem));
foreach (var price in prices)
{
var costItem = price.CostItem?.Trim() ?? string.Empty;
@@ -349,6 +350,44 @@ public partial class ServiceChannelService
}
}
+ ///
+ /// 同一服务渠道下每种费用项只能有一条买价配置。
+ ///
+ private async Task EnsureCostItemUniqueAsync(int serviceChannelId, string? costItem, int? excludePriceId)
+ {
+ var trimmed = costItem?.Trim() ?? string.Empty;
+ if (string.IsNullOrWhiteSpace(trimmed))
+ {
+ throw new Fake.Application.BusinessException("费用项不能为空");
+ }
+
+ var existing = await ServiceChannelRepository.GetPricesAsync(serviceChannelId);
+ var duplicated = existing.Any(p =>
+ (!excludePriceId.HasValue || p.Id != excludePriceId.Value)
+ && string.Equals(p.CostItem?.Trim(), trimmed, StringComparison.OrdinalIgnoreCase));
+ if (duplicated)
+ {
+ throw new Fake.Application.BusinessException(
+ $"该渠道已存在费用项「{trimmed}」,同一渠道下每种费用项只能有一条");
+ }
+ }
+
+ private static void EnsureUniqueCostItemsInBatch(IEnumerable costItems)
+ {
+ var duplicates = costItems
+ .Select(x => x?.Trim() ?? string.Empty)
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .GroupBy(x => x, StringComparer.OrdinalIgnoreCase)
+ .Where(g => g.Count() > 1)
+ .Select(g => g.Key)
+ .ToList();
+ if (duplicates.Count > 0)
+ {
+ throw new Fake.Application.BusinessException(
+ $"同一渠道下费用项不能重复:{string.Join("、", duplicates)}");
+ }
+ }
+
private static ServiceChannelGroupListOutput MapGroupListOutput(ServiceChannelGroup entity)
{
return new ServiceChannelGroupListOutput
diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.cs
index 7188468..137cd20 100644
--- a/src/Oms.Application/Services/Channels/ServiceChannelService.cs
+++ b/src/Oms.Application/Services/Channels/ServiceChannelService.cs
@@ -223,6 +223,7 @@ public partial class ServiceChannelService : ApplicationService
{
await GetEntityOrThrowAsync(input.ServiceChannelId);
ValidateBillingPriceItemRules([input]);
+ await EnsureCostItemUniqueAsync(input.ServiceChannelId, input.CostItem, excludePriceId: null);
await ServiceChannelRepository.InsertPriceAsync(
new ServiceChannelPrice
{
@@ -254,6 +255,7 @@ public partial class ServiceChannelService : ApplicationService
}
ValidateBillingPriceItemRules([input]);
+ await EnsureCostItemUniqueAsync(input.ServiceChannelId, input.CostItem, excludePriceId: price.Id);
price.SerialNumber = input.SerialNumber;
price.Status = input.Status;
price.PriceName = input.PriceName;