Compare commits

...

2 Commits

Author SHA1 Message Date
zzh
b17e6639a4 fix: 主数据新增兼容软删唯一键,避免 Duplicate entry
All checks were successful
Build and Deploy / deploy (push) Successful in 1m3s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-05 11:53:08 +08:00
zzh
79099776a5 feat: 同一产品/渠道下卖价买价费用项唯一,禁止重复新增
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-05 11:23:38 +08:00
31 changed files with 535 additions and 25 deletions

View File

@ -93,6 +93,7 @@ public class AiSkillService : ApplicationService
}
entity ??= await AiSkillRepository.GetBySkillKeyAsync(skillKey);
entity ??= await AiSkillRepository.GetBySkillKeyIncludingDeletedAsync(skillKey);
if (entity == null)
{
@ -106,9 +107,17 @@ public class AiSkillService : ApplicationService
throw new BusinessException($"SkillKey 已存在:{skillKey}");
}
var softDeleted = await AiSkillRepository.GetBySkillKeyIncludingDeletedAsync(skillKey);
if (softDeleted is { IsDeleted: true } && softDeleted.Id != entity.Id)
{
softDeleted.SkillKey = $"{skillKey}__deleted_{softDeleted.Id}";
await AiSkillRepository.UpdateAsync(softDeleted);
}
entity.SkillKey = skillKey;
}
entity.IsDeleted = false;
entity.Scene = scene;
entity.Name = input.Name.Trim();
entity.Description = NormalizeOptional(input.Description);

View File

@ -23,6 +23,21 @@ public partial class SalesProductService
}
}
/// <summary>
/// 更新时若目标编码被软删记录占用,改名释放 uk_sp_code。
/// </summary>
private async Task FreeSoftDeletedProductCodeIfNeededAsync(string productCode, int currentId)
{
var conflict = await SalesProductRepository.GetByProductCodeIncludingDeletedAsync(productCode);
if (conflict == null || !conflict.IsDeleted || conflict.Id == currentId)
{
return;
}
conflict.ProductCode = $"{productCode}__deleted_{conflict.Id}";
await SalesProductRepository.UpdateAsync(conflict);
}
/// <summary>
/// 获取销售产品,不存在时直接抛异常。
/// </summary>
@ -388,10 +403,11 @@ public partial class SalesProductService
};
/// <summary>
/// 保险/偏远费等附加费用项的计费条件校验。
/// 保险/偏远费等附加费用项的计费条件校验;同一提交批次内费用项不可重复。
/// </summary>
private static void ValidateBillingPriceItemRules(IReadOnlyList<SalesProductPriceSaveInput> prices)
{
EnsureUniqueCostItemsInBatch(prices.Select(x => x.CostItem));
foreach (var price in prices)
{
var costItem = price.CostItem?.Trim() ?? string.Empty;
@ -417,6 +433,44 @@ public partial class SalesProductService
}
}
/// <summary>
/// 同一销售产品下每种费用项只能有一条卖价配置。
/// </summary>
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<string?> 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))

View File

@ -75,6 +75,16 @@ public partial class SalesProductService : ApplicationService
public async Task CreateAsync(SalesProductSaveInput input)
{
await ValidateProductCodeAsync(input.ProductCode, null);
var code = input.ProductCode.Trim();
var deleted = await SalesProductRepository.GetByProductCodeIncludingDeletedAsync(code);
if (deleted != null && deleted.IsDeleted)
{
deleted.IsDeleted = false;
ApplyProductInput(deleted, input);
await SalesProductRepository.UpdateAsync(deleted);
return;
}
var entity = new SalesProduct();
ApplyProductInput(entity, input);
await SalesProductRepository.InsertAsync(entity);
@ -90,6 +100,7 @@ public partial class SalesProductService : ApplicationService
{
var entity = await GetProductOrThrowAsync(input.Id);
await ValidateProductCodeAsync(input.ProductCode, entity.Id);
await FreeSoftDeletedProductCodeIfNeededAsync(input.ProductCode.Trim(), entity.Id);
ApplyProductInput(entity, input);
await SalesProductRepository.UpdateAsync(entity);
}
@ -159,6 +170,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 +202,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;

View File

@ -91,6 +91,21 @@ public partial class ServiceChannelService
}
}
/// <summary>
/// 更新时若目标编码被软删记录占用,改名释放 uk_sc_code。
/// </summary>
private async Task FreeSoftDeletedChannelCodeIfNeededAsync(string channelCode, int currentId)
{
var conflict = await ServiceChannelRepository.GetByChannelCodeIncludingDeletedAsync(channelCode);
if (conflict == null || !conflict.IsDeleted || conflict.Id == currentId)
{
return;
}
conflict.ChannelCode = $"{channelCode}__deleted_{conflict.Id}";
await ServiceChannelRepository.UpdateAsync(conflict);
}
/// <summary>
/// 获取服务渠道,不存在时直接抛异常。
/// </summary>
@ -329,10 +344,11 @@ public partial class ServiceChannelService
}
/// <summary>
/// 买价偏远费等附加费用项的计费条件校验。
/// 买价偏远费等附加费用项的计费条件校验;同一提交批次内费用项不可重复。
/// </summary>
private static void ValidateBillingPriceItemRules(IReadOnlyList<ServiceChannelPriceSaveInput> prices)
{
EnsureUniqueCostItemsInBatch(prices.Select(x => x.CostItem));
foreach (var price in prices)
{
var costItem = price.CostItem?.Trim() ?? string.Empty;
@ -349,6 +365,44 @@ public partial class ServiceChannelService
}
}
/// <summary>
/// 同一服务渠道下每种费用项只能有一条买价配置。
/// </summary>
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<string?> 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

View File

@ -73,6 +73,16 @@ public partial class ServiceChannelService : ApplicationService
public async Task CreateAsync(ServiceChannelSaveInput input)
{
await ValidateChannelCodeAsync(input.ChannelCode, null);
var code = input.ChannelCode.Trim();
var deleted = await ServiceChannelRepository.GetByChannelCodeIncludingDeletedAsync(code);
if (deleted != null && deleted.IsDeleted)
{
deleted.IsDeleted = false;
ApplyInput(deleted, input);
await ServiceChannelRepository.UpdateAsync(deleted);
return;
}
var entity = new ServiceChannel();
ApplyInput(entity, input);
await ServiceChannelRepository.InsertAsync(entity);
@ -88,6 +98,7 @@ public partial class ServiceChannelService : ApplicationService
{
var entity = await GetEntityOrThrowAsync(input.Id);
await ValidateChannelCodeAsync(input.ChannelCode, entity.Id);
await FreeSoftDeletedChannelCodeIfNeededAsync(input.ChannelCode.Trim(), entity.Id);
ApplyInput(entity, input);
await ServiceChannelRepository.UpdateAsync(entity);
}
@ -223,6 +234,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 +266,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;

View File

@ -178,6 +178,12 @@ public class CustomerService : ApplicationService
NormalizeRequiredText(input.Email, "邮箱"),
WeChatRegistrationFieldNormalizer.EmailMaxLength);
await FreeSoftDeletedCustomerIdentityIfNeededAsync(
customer.CustomerCode,
phone,
email,
excludeId: customer.Id);
var duplicatedPhone = await CustomerRepository.GetByPhoneAsync(phone, cancellationToken);
if (duplicatedPhone != null && duplicatedPhone.Id != customer.Id)
{
@ -216,6 +222,10 @@ public class CustomerService : ApplicationService
{
var normalized = await NormalizeCreateCustomerInputAsync(input, "客户编号", "客户名称");
await FreeSoftDeletedCustomerIdentityIfNeededAsync(
normalized.CustomerCode,
normalized.Phone,
normalized.Email);
await EnsureCustomerIdentityUniqueAsync(normalized.CustomerCode, normalized.Phone, normalized.Email);
var customerRoleId = await GetCustomerRoleIdOrThrowAsync();
@ -268,6 +278,10 @@ public class CustomerService : ApplicationService
var normalized = await NormalizeCreateCustomerInputAsync(input, "子客户编号", "子客户名称");
var permissions = await NormalizeAndValidateSubCustomerPermissionsAsync(input.Permissions);
await FreeSoftDeletedCustomerIdentityIfNeededAsync(
normalized.CustomerCode,
normalized.Phone,
normalized.Email);
await EnsureCustomerIdentityUniqueAsync(normalized.CustomerCode, normalized.Phone, normalized.Email);
var user = await CreateRbacUserAsync(input, normalized);
@ -550,8 +564,46 @@ public class CustomerService : ApplicationService
}
}
/// <summary>
/// 客户创建会新建 RBAC 用户,不适合直接恢复软删行;释放软删占用的唯一键即可。
/// </summary>
private async Task FreeSoftDeletedCustomerIdentityIfNeededAsync(
string customerCode,
string phone,
string? email,
int? excludeId = null)
{
var byCode = await CustomerRepository.GetByCustomerCodeIncludingDeletedAsync(customerCode);
if (byCode != null && byCode.IsDeleted && byCode.Id != excludeId)
{
byCode.CustomerCode = $"{customerCode}__deleted_{byCode.Id}";
await CustomerRepository.UpdateAsync(byCode);
}
var byPhone = await CustomerRepository.GetByPhoneIncludingDeletedAsync(phone);
if (byPhone != null && byPhone.IsDeleted && byPhone.Id != excludeId)
{
byPhone.Phone = $"{phone}__deleted_{byPhone.Id}";
await CustomerRepository.UpdateAsync(byPhone);
}
if (string.IsNullOrWhiteSpace(email))
{
return;
}
var byEmail = await CustomerRepository.GetByEmailIncludingDeletedAsync(email);
if (byEmail != null && byEmail.IsDeleted && byEmail.Id != excludeId)
{
byEmail.Email = $"{email}__deleted_{byEmail.Id}";
await CustomerRepository.UpdateAsync(byEmail);
}
}
private async Task EnsureCustomerIdentityUniqueForUpdateAsync(int id, string customerCode, string phone, string? email)
{
await FreeSoftDeletedCustomerIdentityIfNeededAsync(customerCode, phone, email, excludeId: id);
var duplicatedCode = await CustomerRepository.GetByCustomerCodeAsync(customerCode);
if (duplicatedCode != null && duplicatedCode.Id != id)
{

View File

@ -64,7 +64,22 @@ public class KbCategoryService : ApplicationService
throw new BusinessException($"分类编码已存在:{code}");
}
entity ??= new KbCategory();
if (entity == null)
{
var deleted = await CategoryRepository.GetByCodeIncludingDeletedAsync(code);
entity = deleted is { IsDeleted: true } ? deleted : new KbCategory();
}
else
{
var softDeleted = await CategoryRepository.GetByCodeIncludingDeletedAsync(code);
if (softDeleted is { IsDeleted: true } && softDeleted.Id != entity.Id)
{
softDeleted.Code = $"{code}__deleted_{softDeleted.Id}";
await CategoryRepository.UpdateAsync(softDeleted);
}
}
entity.IsDeleted = false;
entity.Code = code;
entity.Name = name;
entity.SortOrder = input.SortOrder;

View File

@ -132,14 +132,10 @@ public class DictService : ApplicationService
}
entity ??= await DictTypeRepository.GetByDictTypeAsync(dictType);
// 软删后列表查不到,但 uk_dict_type_code 仍占坑;重建时恢复原行
entity ??= await DictTypeRepository.GetByDictTypeIncludingDeletedAsync(dictType);
if (entity == null)
{
var existedType = await DictTypeRepository.GetByDictTypeAsync(dictType);
if (existedType != null)
{
throw new BusinessException("字典类型已存在");
}
await DictTypeRepository.InsertAsync(new DictTypeMeta
{
DictType = dictType,
@ -151,13 +147,19 @@ public class DictService : ApplicationService
if (!string.Equals(entity.DictType, dictType, StringComparison.OrdinalIgnoreCase))
{
var duplicate = await DictTypeRepository.GetByDictTypeAsync(dictType);
if (duplicate != null && duplicate.Id != entity.Id)
var duplicate = await DictTypeRepository.GetByDictTypeIncludingDeletedAsync(dictType);
if (duplicate != null && duplicate.Id != entity.Id && !duplicate.IsDeleted)
{
throw new BusinessException("字典类型已存在");
}
if (duplicate != null && duplicate.Id != entity.Id && duplicate.IsDeleted)
{
throw new BusinessException("字典类型编码与已删除记录冲突,请换编码或联系管理员恢复");
}
}
entity.IsDeleted = false;
entity.DictType = dictType;
entity.Remark = remark;
entity.TypeSort = input.TypeSort;
@ -317,6 +319,14 @@ public class DictService : ApplicationService
return;
}
var deleted = await DictTypeRepository.GetByDictTypeIncludingDeletedAsync(dictType);
if (deleted != null)
{
deleted.IsDeleted = false;
await DictTypeRepository.UpdateAsync(deleted);
return;
}
await DictTypeRepository.InsertAsync(new DictTypeMeta
{
DictType = dictType,

View File

@ -61,9 +61,26 @@ public class SystemOtherConfigService : ApplicationService
public async Task CreateAsync(SystemOtherConfigCreateInput input)
{
await ValidateInputAsync(input, null);
var configCode = input.ConfigCode.Trim();
var deleted = await SystemOtherConfigRepository.GetByConfigCodeIncludingDeletedAsync(configCode);
if (deleted != null && deleted.IsDeleted)
{
deleted.IsDeleted = false;
deleted.ConfigCode = configCode;
deleted.ConfigName = input.ConfigName.Trim();
deleted.Enabled = input.Enabled;
deleted.BaseUrl = NormalizeOptional(input.BaseUrl);
deleted.AppKey = NormalizeOptional(input.AppKey);
deleted.AppSecret = NormalizeOptional(input.AppSecret);
deleted.TimeoutSeconds = NormalizeTimeoutSeconds(input.TimeoutSeconds);
deleted.Remark = NormalizeOptional(input.Remark);
await SystemOtherConfigRepository.UpdateAsync(deleted);
return;
}
await SystemOtherConfigRepository.InsertAsync(new SystemOtherConfig
{
ConfigCode = input.ConfigCode.Trim(),
ConfigCode = configCode,
ConfigName = input.ConfigName.Trim(),
Enabled = input.Enabled,
BaseUrl = NormalizeOptional(input.BaseUrl),
@ -82,8 +99,16 @@ public class SystemOtherConfigService : ApplicationService
{
var entity = await GetConfigOrThrowAsync(input.Id);
await ValidateInputAsync(input, entity.Id);
var configCode = input.ConfigCode.Trim();
var softDeleted = await SystemOtherConfigRepository.GetByConfigCodeIncludingDeletedAsync(configCode);
if (softDeleted != null && softDeleted.IsDeleted && softDeleted.Id != entity.Id)
{
softDeleted.ConfigCode = $"{configCode}__deleted_{softDeleted.Id}";
await SystemOtherConfigRepository.UpdateAsync(softDeleted);
}
var clearWeChatAccessToken = ShouldClearWeChatAccessToken(entity, input);
entity.ConfigCode = input.ConfigCode.Trim();
entity.ConfigCode = configCode;
entity.ConfigName = input.ConfigName.Trim();
entity.Enabled = input.Enabled;
entity.BaseUrl = NormalizeOptional(input.BaseUrl);

View File

@ -81,15 +81,29 @@ public class WarehouseService : ApplicationService
[HttpPost("/system/warehouse/create")]
public async Task CreateAsync(CreateWarehouseInput input)
{
var duplicatedWarehouse = await WarehouseAddressRepository.GetByWarehouseNameAsync(input.WarehouseName);
var warehouseName = input.WarehouseName.Trim();
var duplicatedWarehouse = await WarehouseAddressRepository.GetByWarehouseNameAsync(warehouseName);
if (duplicatedWarehouse != null)
{
throw new BusinessException("仓库名称已存在");
}
var deleted = await WarehouseAddressRepository.GetByWarehouseNameIncludingDeletedAsync(warehouseName);
if (deleted != null && deleted.IsDeleted)
{
deleted.IsDeleted = false;
deleted.WarehouseName = warehouseName;
deleted.WarehouseType = input.WarehouseType;
deleted.Recipient = input.Recipient;
deleted.ContactPhone = input.ContactPhone;
deleted.ShippingAddress = input.ShippingAddress;
await WarehouseAddressRepository.UpdateAsync(deleted);
return;
}
var entity = new WarehouseAddress
{
WarehouseName = input.WarehouseName,
WarehouseName = warehouseName,
WarehouseType = input.WarehouseType,
Recipient = input.Recipient,
ContactPhone = input.ContactPhone,
@ -108,13 +122,21 @@ public class WarehouseService : ApplicationService
public async Task UpdateAsync(UpdateWarehouseInput input)
{
var entity = await GetWarehouseOrThrowAsync(input.Id);
var duplicatedWarehouse = await WarehouseAddressRepository.GetByWarehouseNameAsync(input.WarehouseName);
var warehouseName = input.WarehouseName.Trim();
var duplicatedWarehouse = await WarehouseAddressRepository.GetByWarehouseNameAsync(warehouseName);
if (duplicatedWarehouse != null && duplicatedWarehouse.Id != entity.Id)
{
throw new BusinessException("仓库名称已存在");
}
entity.WarehouseName = input.WarehouseName;
var softDeleted = await WarehouseAddressRepository.GetByWarehouseNameIncludingDeletedAsync(warehouseName);
if (softDeleted != null && softDeleted.IsDeleted && softDeleted.Id != entity.Id)
{
softDeleted.WarehouseName = $"{warehouseName}__deleted_{softDeleted.Id}";
await WarehouseAddressRepository.UpdateAsync(softDeleted);
}
entity.WarehouseName = warehouseName;
entity.WarehouseType = input.WarehouseType;
entity.Recipient = input.Recipient;
entity.ContactPhone = input.ContactPhone;

View File

@ -63,7 +63,12 @@ public partial class WarehouseService
}
else
{
entity = new CBinLocation { WarehouseId = input.WarehouseId };
var deleted = await BinLocationRepository.GetByWarehouseAndBinCodeIncludingDeletedAsync(
input.WarehouseId,
binCode);
entity = deleted is { IsDeleted: true }
? deleted
: new CBinLocation { WarehouseId = input.WarehouseId };
}
var duplicate = await BinLocationRepository.FirstOrDefaultAsync(x =>
@ -73,13 +78,26 @@ public partial class WarehouseService
throw new BusinessException("库位编码已存在");
}
if (input.Id > 0)
{
var softDeleted = await BinLocationRepository.GetByWarehouseAndBinCodeIncludingDeletedAsync(
input.WarehouseId,
binCode);
if (softDeleted is { IsDeleted: true } && softDeleted.Id != entity.Id)
{
softDeleted.BinCode = $"{binCode}__deleted_{softDeleted.Id}";
await BinLocationRepository.UpdateAsync(softDeleted);
}
}
entity.IsDeleted = false;
entity.BinCode = binCode;
entity.BinName = string.IsNullOrWhiteSpace(input.BinName) ? null : input.BinName.Trim();
entity.ZoneCode = string.IsNullOrWhiteSpace(input.ZoneCode) ? null : input.ZoneCode.Trim();
entity.IsEnabled = input.IsEnabled;
entity.Remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim();
if (input.Id > 0)
if (entity.Id > 0)
{
await BinLocationRepository.UpdateAsync(entity);
}

View File

@ -105,7 +105,10 @@ public partial class WarehouseService
}
else
{
entity = new Domain.WarehouseAggregate.Entities.CSku { CustomerId = customerId };
var deleted = await SkuRepository.GetByCustomerAndSkuCodeIncludingDeletedAsync(customerId, skuCode);
entity = deleted is { IsDeleted: true }
? deleted
: new Domain.WarehouseAggregate.Entities.CSku { CustomerId = customerId };
}
var duplicate = await SkuRepository.FirstOrDefaultAsync(x =>
@ -115,6 +118,17 @@ public partial class WarehouseService
throw new BusinessException("SKU 编码已存在");
}
if (input.Id > 0)
{
var softDeleted = await SkuRepository.GetByCustomerAndSkuCodeIncludingDeletedAsync(customerId, skuCode);
if (softDeleted is { IsDeleted: true } && softDeleted.Id != entity.Id)
{
softDeleted.SkuCode = $"{skuCode}__deleted_{softDeleted.Id}";
await SkuRepository.UpdateAsync(softDeleted);
}
}
entity.IsDeleted = false;
entity.SkuCode = skuCode;
entity.SkuName = skuName;
entity.SkuNameEn = string.IsNullOrWhiteSpace(input.SkuNameEn) ? null : input.SkuNameEn.Trim();
@ -129,7 +143,7 @@ public partial class WarehouseService
entity.HeightCm = input.HeightCm;
entity.Remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim();
if (input.Id > 0)
if (entity.Id > 0)
{
await SkuRepository.UpdateAsync(entity);
}

View File

@ -6,4 +6,8 @@ namespace Oms.Domain.AiAggregate.Repositories;
public interface IAiSkillRepository : IRepository<AiSkill>
{
Task<AiSkill?> GetBySkillKeyAsync(string skillKey, CancellationToken cancellationToken = default);
Task<AiSkill?> GetBySkillKeyIncludingDeletedAsync(
string skillKey,
CancellationToken cancellationToken = default);
}

View File

@ -7,6 +7,10 @@ public interface ISalesProductRepository : IRepository<SalesProduct>
{
Task<SalesProduct?> GetByProductCodeAsync(string productCode, CancellationToken cancellationToken = default);
Task<SalesProduct?> GetByProductCodeIncludingDeletedAsync(
string productCode,
CancellationToken cancellationToken = default);
Task<bool> HasProductCodeAsync(string productCode, CancellationToken cancellationToken = default);
Task<List<SalesProductPrice>> GetPricesAsync(int salesProductId, CancellationToken cancellationToken = default);

View File

@ -7,6 +7,10 @@ public interface IServiceChannelRepository : IRepository<ServiceChannel>
{
Task<ServiceChannel?> GetByChannelCodeAsync(string channelCode, CancellationToken cancellationToken = default);
Task<ServiceChannel?> GetByChannelCodeIncludingDeletedAsync(
string channelCode,
CancellationToken cancellationToken = default);
Task<bool> HasChannelCodeAsync(string channelCode, CancellationToken cancellationToken = default);
Task<List<ServiceChannelGroup>> GetGroupsAsync(CancellationToken cancellationToken = default);

View File

@ -7,10 +7,18 @@ public interface ICustomerRepository : IRepository<Customer>
{
Task<Customer?> GetByCustomerCodeAsync(string customerCode, CancellationToken cancellationToken = default);
Task<Customer?> GetByCustomerCodeIncludingDeletedAsync(
string customerCode,
CancellationToken cancellationToken = default);
Task<Customer?> GetByPhoneAsync(string phone, CancellationToken cancellationToken = default);
Task<Customer?> GetByPhoneIncludingDeletedAsync(string phone, CancellationToken cancellationToken = default);
Task<Customer?> GetByEmailAsync(string email, CancellationToken cancellationToken = default);
Task<Customer?> GetByEmailIncludingDeletedAsync(string email, CancellationToken cancellationToken = default);
Task<Customer?> GetByWechatOpenidAsync(string wechatOpenid, CancellationToken cancellationToken = default);
Task<bool> ExistsPhoneAsync(string phone, CancellationToken cancellationToken = default);

View File

@ -7,5 +7,7 @@ public interface IKbCategoryRepository : IRepository<KbCategory>
{
Task<KbCategory?> GetByCodeAsync(string code, CancellationToken cancellationToken = default);
Task<KbCategory?> GetByCodeIncludingDeletedAsync(string code, CancellationToken cancellationToken = default);
Task<bool> ExistsCodeAsync(string code, int? excludeId = null, CancellationToken cancellationToken = default);
}

View File

@ -6,4 +6,11 @@ namespace Oms.Domain.SystemAggregate.Repositories;
public interface IDictTypeRepository : IRepository<DictTypeMeta>
{
Task<DictTypeMeta?> GetByDictTypeAsync(string dictType, CancellationToken cancellationToken = default);
/// <summary>
/// 按类型编码查询(含软删除),用于唯一键冲突时恢复。
/// </summary>
Task<DictTypeMeta?> GetByDictTypeIncludingDeletedAsync(
string dictType,
CancellationToken cancellationToken = default);
}

View File

@ -6,4 +6,8 @@ namespace Oms.Domain.SystemAggregate.Repositories;
public interface ISystemOtherConfigRepository : IRepository<SystemOtherConfig>
{
Task<SystemOtherConfig?> GetByConfigCodeAsync(string configCode, CancellationToken cancellationToken = default);
Task<SystemOtherConfig?> GetByConfigCodeIncludingDeletedAsync(
string configCode,
CancellationToken cancellationToken = default);
}

View File

@ -7,5 +7,9 @@ public interface IWarehouseAddressRepository : IRepository<WarehouseAddress>
{
Task<WarehouseAddress?> GetByWarehouseNameAsync(string warehouseName, CancellationToken cancellationToken = default);
Task<WarehouseAddress?> GetByWarehouseNameIncludingDeletedAsync(
string warehouseName,
CancellationToken cancellationToken = default);
Task<bool> ExistsWarehouseNameAsync(string warehouseName, CancellationToken cancellationToken = default);
}

View File

@ -3,9 +3,21 @@ using Oms.Domain.WarehouseAggregate.Entities;
namespace Oms.Domain.WarehouseAggregate.Repositories;
public interface ICSkuRepository : IRepository<CSku>;
public interface ICSkuRepository : IRepository<CSku>
{
Task<CSku?> GetByCustomerAndSkuCodeIncludingDeletedAsync(
int customerId,
string skuCode,
CancellationToken cancellationToken = default);
}
public interface ICBinLocationRepository : IRepository<CBinLocation>;
public interface ICBinLocationRepository : IRepository<CBinLocation>
{
Task<CBinLocation?> GetByWarehouseAndBinCodeIncludingDeletedAsync(
int warehouseId,
string binCode,
CancellationToken cancellationToken = default);
}
public interface ICInventoryRepository : IRepository<CInventory>;

View File

@ -12,4 +12,15 @@ public class AiSkillRepository
string skillKey,
CancellationToken cancellationToken = default) =>
FirstOrDefaultAsync(x => x.SkillKey == skillKey, cancellationToken);
public async Task<AiSkill?> GetBySkillKeyIncludingDeletedAsync(
string skillKey,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
return await SoftDeleteQuery.FirstIncludingDeletedAsync<AiSkill>(
db,
x => x.SkillKey == skillKey,
cancellationToken);
}
}

View File

@ -13,6 +13,17 @@ public class SalesProductRepository : OmsSqlSugarRepository<SalesProduct>, ISale
return FirstOrDefaultAsync(x => x.ProductCode == productCode, cancellationToken);
}
public async Task<SalesProduct?> GetByProductCodeIncludingDeletedAsync(
string productCode,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
return await SoftDeleteQuery.FirstIncludingDeletedAsync<SalesProduct>(
db,
x => x.ProductCode == productCode,
cancellationToken);
}
public Task<bool> HasProductCodeAsync(string productCode, CancellationToken cancellationToken = default)
{
return AnyAsync(x => x.ProductCode == productCode, cancellationToken);

View File

@ -13,6 +13,17 @@ public class ServiceChannelRepository : OmsSqlSugarRepository<ServiceChannel>, I
return FirstOrDefaultAsync(x => x.ChannelCode == channelCode, cancellationToken);
}
public async Task<ServiceChannel?> GetByChannelCodeIncludingDeletedAsync(
string channelCode,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
return await SoftDeleteQuery.FirstIncludingDeletedAsync<ServiceChannel>(
db,
x => x.ChannelCode == channelCode,
cancellationToken);
}
public Task<bool> HasChannelCodeAsync(string channelCode, CancellationToken cancellationToken = default)
{
return AnyAsync(x => x.ChannelCode == channelCode, cancellationToken);

View File

@ -12,16 +12,49 @@ public class CustomerRepository : OmsSqlSugarRepository<Customer>, ICustomerRepo
return FirstOrDefaultAsync(x => x.CustomerCode == customerCode, cancellationToken);
}
public async Task<Customer?> GetByCustomerCodeIncludingDeletedAsync(
string customerCode,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
return await SoftDeleteQuery.FirstIncludingDeletedAsync<Customer>(
db,
x => x.CustomerCode == customerCode,
cancellationToken);
}
public Task<Customer?> GetByPhoneAsync(string phone, CancellationToken cancellationToken = default)
{
return FirstOrDefaultAsync(x => x.Phone == phone, cancellationToken);
}
public async Task<Customer?> GetByPhoneIncludingDeletedAsync(
string phone,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
return await SoftDeleteQuery.FirstIncludingDeletedAsync<Customer>(
db,
x => x.Phone == phone,
cancellationToken);
}
public Task<Customer?> GetByEmailAsync(string email, CancellationToken cancellationToken = default)
{
return FirstOrDefaultAsync(x => x.Email == email, cancellationToken);
}
public async Task<Customer?> GetByEmailIncludingDeletedAsync(
string email,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
return await SoftDeleteQuery.FirstIncludingDeletedAsync<Customer>(
db,
x => x.Email == email,
cancellationToken);
}
public Task<Customer?> GetByWechatOpenidAsync(string wechatOpenid, CancellationToken cancellationToken = default)
{
return FirstOrDefaultAsync(x => x.WechatOpenid == wechatOpenid, cancellationToken);

View File

@ -11,6 +11,17 @@ public class KbCategoryRepository
public Task<KbCategory?> GetByCodeAsync(string code, CancellationToken cancellationToken = default) =>
FirstOrDefaultAsync(x => x.Code == code, cancellationToken);
public async Task<KbCategory?> GetByCodeIncludingDeletedAsync(
string code,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
return await SoftDeleteQuery.FirstIncludingDeletedAsync<KbCategory>(
db,
x => x.Code == code,
cancellationToken);
}
public async Task<bool> ExistsCodeAsync(
string code,
int? excludeId = null,

View File

@ -0,0 +1,24 @@
using System.Linq.Expressions;
using SqlSugar;
namespace Oms.SqlSugarCore.Repositories;
/// <summary>
/// 软删场景下按业务唯一键查询(ClearFilter),供新增恢复 / 释放唯一键使用。
/// </summary>
public static class SoftDeleteQuery
{
public static async Task<TEntity?> FirstIncludingDeletedAsync<TEntity>(
ISqlSugarClient db,
Expression<Func<TEntity, bool>> predicate,
CancellationToken cancellationToken = default)
where TEntity : class, new()
{
var rows = await db.Queryable<TEntity>()
.ClearFilter()
.Where(predicate)
.Take(1)
.ToListAsync(cancellationToken);
return rows.FirstOrDefault();
}
}

View File

@ -11,4 +11,17 @@ public class DictTypeRepository : OmsSqlSugarRepository<DictTypeMeta>, IDictType
{
return FirstOrDefaultAsync(x => x.DictType == dictType, cancellationToken);
}
public async Task<DictTypeMeta?> GetByDictTypeIncludingDeletedAsync(
string dictType,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
var rows = await db.Queryable<DictTypeMeta>()
.ClearFilter()
.Where(x => x.DictType == dictType)
.Take(1)
.ToListAsync(cancellationToken);
return rows.FirstOrDefault();
}
}

View File

@ -11,4 +11,15 @@ public class SystemOtherConfigRepository : OmsSqlSugarRepository<SystemOtherConf
{
return FirstOrDefaultAsync(x => x.ConfigCode == configCode, cancellationToken);
}
public async Task<SystemOtherConfig?> GetByConfigCodeIncludingDeletedAsync(
string configCode,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
return await SoftDeleteQuery.FirstIncludingDeletedAsync<SystemOtherConfig>(
db,
x => x.ConfigCode == configCode,
cancellationToken);
}
}

View File

@ -12,6 +12,17 @@ public class WarehouseAddressRepository : OmsSqlSugarRepository<WarehouseAddress
return FirstOrDefaultAsync(x => x.WarehouseName == warehouseName, cancellationToken);
}
public async Task<WarehouseAddress?> GetByWarehouseNameIncludingDeletedAsync(
string warehouseName,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
return await SoftDeleteQuery.FirstIncludingDeletedAsync<WarehouseAddress>(
db,
x => x.WarehouseName == warehouseName,
cancellationToken);
}
public Task<bool> ExistsWarehouseNameAsync(string warehouseName, CancellationToken cancellationToken = default)
{
return AnyAsync(x => x.WarehouseName == warehouseName, cancellationToken);

View File

@ -5,9 +5,35 @@ using Oms.SqlSugarCore.Repositories;
namespace Oms.SqlSugarCore.Warehouse;
public class CSkuRepository : OmsSqlSugarRepository<CSku>, ICSkuRepository, IScopedDependency;
public class CSkuRepository : OmsSqlSugarRepository<CSku>, ICSkuRepository, IScopedDependency
{
public async Task<CSku?> GetByCustomerAndSkuCodeIncludingDeletedAsync(
int customerId,
string skuCode,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
return await SoftDeleteQuery.FirstIncludingDeletedAsync<CSku>(
db,
x => x.CustomerId == customerId && x.SkuCode == skuCode,
cancellationToken);
}
}
public class CBinLocationRepository : OmsSqlSugarRepository<CBinLocation>, ICBinLocationRepository, IScopedDependency;
public class CBinLocationRepository : OmsSqlSugarRepository<CBinLocation>, ICBinLocationRepository, IScopedDependency
{
public async Task<CBinLocation?> GetByWarehouseAndBinCodeIncludingDeletedAsync(
int warehouseId,
string binCode,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
return await SoftDeleteQuery.FirstIncludingDeletedAsync<CBinLocation>(
db,
x => x.WarehouseId == warehouseId && x.BinCode == binCode,
cancellationToken);
}
}
public class CInventoryRepository : OmsSqlSugarRepository<CInventory>, ICInventoryRepository, IScopedDependency;