diff --git a/src/Oms.Application/Services/Ai/AiSkillService.cs b/src/Oms.Application/Services/Ai/AiSkillService.cs index 775d83f..ce85056 100644 --- a/src/Oms.Application/Services/Ai/AiSkillService.cs +++ b/src/Oms.Application/Services/Ai/AiSkillService.cs @@ -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); diff --git a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs index 5cf568d..5c3203d 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs @@ -23,6 +23,21 @@ public partial class SalesProductService } } + /// + /// 更新时若目标编码被软删记录占用,改名释放 uk_sp_code。 + /// + 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); + } + /// /// 获取销售产品,不存在时直接抛异常。 /// diff --git a/src/Oms.Application/Services/Channels/SalesProductService.cs b/src/Oms.Application/Services/Channels/SalesProductService.cs index 29ddaf2..9ebf67f 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.cs @@ -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); } diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs index 0f8c182..eb303e8 100644 --- a/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/ServiceChannelService.Helpers.cs @@ -91,6 +91,21 @@ public partial class ServiceChannelService } } + /// + /// 更新时若目标编码被软删记录占用,改名释放 uk_sc_code。 + /// + 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); + } + /// /// 获取服务渠道,不存在时直接抛异常。 /// diff --git a/src/Oms.Application/Services/Channels/ServiceChannelService.cs b/src/Oms.Application/Services/Channels/ServiceChannelService.cs index 137cd20..061ab28 100644 --- a/src/Oms.Application/Services/Channels/ServiceChannelService.cs +++ b/src/Oms.Application/Services/Channels/ServiceChannelService.cs @@ -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); } diff --git a/src/Oms.Application/Services/Customers/CustomerService.cs b/src/Oms.Application/Services/Customers/CustomerService.cs index 21550cd..8e6bed7 100644 --- a/src/Oms.Application/Services/Customers/CustomerService.cs +++ b/src/Oms.Application/Services/Customers/CustomerService.cs @@ -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 } } + /// + /// 客户创建会新建 RBAC 用户,不适合直接恢复软删行;释放软删占用的唯一键即可。 + /// + 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) { diff --git a/src/Oms.Application/Services/Kb/KbCategoryService.cs b/src/Oms.Application/Services/Kb/KbCategoryService.cs index a42186e..492845c 100644 --- a/src/Oms.Application/Services/Kb/KbCategoryService.cs +++ b/src/Oms.Application/Services/Kb/KbCategoryService.cs @@ -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; diff --git a/src/Oms.Application/Services/System/DictService.cs b/src/Oms.Application/Services/System/DictService.cs index 2bd2737..7329353 100644 --- a/src/Oms.Application/Services/System/DictService.cs +++ b/src/Oms.Application/Services/System/DictService.cs @@ -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, diff --git a/src/Oms.Application/Services/System/SystemOtherConfigService.cs b/src/Oms.Application/Services/System/SystemOtherConfigService.cs index b213f87..7c52138 100644 --- a/src/Oms.Application/Services/System/SystemOtherConfigService.cs +++ b/src/Oms.Application/Services/System/SystemOtherConfigService.cs @@ -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); diff --git a/src/Oms.Application/Services/System/WarehouseService.cs b/src/Oms.Application/Services/System/WarehouseService.cs index 2e27f6c..8532769 100644 --- a/src/Oms.Application/Services/System/WarehouseService.cs +++ b/src/Oms.Application/Services/System/WarehouseService.cs @@ -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; diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.BinLocation.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.BinLocation.cs index 0c1327e..3fe21b9 100644 --- a/src/Oms.Application/Services/Warehouse/WarehouseService.BinLocation.cs +++ b/src/Oms.Application/Services/Warehouse/WarehouseService.BinLocation.cs @@ -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); } diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs index 646e35f..c0b0eaa 100644 --- a/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs +++ b/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs @@ -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); } diff --git a/src/Oms.Domain/AiAggregate/Repositories/IAiSkillRepository.cs b/src/Oms.Domain/AiAggregate/Repositories/IAiSkillRepository.cs index 8dfae78..bf79a04 100644 --- a/src/Oms.Domain/AiAggregate/Repositories/IAiSkillRepository.cs +++ b/src/Oms.Domain/AiAggregate/Repositories/IAiSkillRepository.cs @@ -6,4 +6,8 @@ namespace Oms.Domain.AiAggregate.Repositories; public interface IAiSkillRepository : IRepository { Task GetBySkillKeyAsync(string skillKey, CancellationToken cancellationToken = default); + + Task GetBySkillKeyIncludingDeletedAsync( + string skillKey, + CancellationToken cancellationToken = default); } diff --git a/src/Oms.Domain/ChannelsAggregate/Repositories/ISalesProductRepository.cs b/src/Oms.Domain/ChannelsAggregate/Repositories/ISalesProductRepository.cs index ea41dad..5ba942a 100644 --- a/src/Oms.Domain/ChannelsAggregate/Repositories/ISalesProductRepository.cs +++ b/src/Oms.Domain/ChannelsAggregate/Repositories/ISalesProductRepository.cs @@ -7,6 +7,10 @@ public interface ISalesProductRepository : IRepository { Task GetByProductCodeAsync(string productCode, CancellationToken cancellationToken = default); + Task GetByProductCodeIncludingDeletedAsync( + string productCode, + CancellationToken cancellationToken = default); + Task HasProductCodeAsync(string productCode, CancellationToken cancellationToken = default); Task> GetPricesAsync(int salesProductId, CancellationToken cancellationToken = default); diff --git a/src/Oms.Domain/ChannelsAggregate/Repositories/IServiceChannelRepository.cs b/src/Oms.Domain/ChannelsAggregate/Repositories/IServiceChannelRepository.cs index 0ae8a9f..bb090e5 100644 --- a/src/Oms.Domain/ChannelsAggregate/Repositories/IServiceChannelRepository.cs +++ b/src/Oms.Domain/ChannelsAggregate/Repositories/IServiceChannelRepository.cs @@ -7,6 +7,10 @@ public interface IServiceChannelRepository : IRepository { Task GetByChannelCodeAsync(string channelCode, CancellationToken cancellationToken = default); + Task GetByChannelCodeIncludingDeletedAsync( + string channelCode, + CancellationToken cancellationToken = default); + Task HasChannelCodeAsync(string channelCode, CancellationToken cancellationToken = default); Task> GetGroupsAsync(CancellationToken cancellationToken = default); diff --git a/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerRepository.cs b/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerRepository.cs index 7292f30..a46ba16 100644 --- a/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerRepository.cs +++ b/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerRepository.cs @@ -7,10 +7,18 @@ public interface ICustomerRepository : IRepository { Task GetByCustomerCodeAsync(string customerCode, CancellationToken cancellationToken = default); + Task GetByCustomerCodeIncludingDeletedAsync( + string customerCode, + CancellationToken cancellationToken = default); + Task GetByPhoneAsync(string phone, CancellationToken cancellationToken = default); + Task GetByPhoneIncludingDeletedAsync(string phone, CancellationToken cancellationToken = default); + Task GetByEmailAsync(string email, CancellationToken cancellationToken = default); + Task GetByEmailIncludingDeletedAsync(string email, CancellationToken cancellationToken = default); + Task GetByWechatOpenidAsync(string wechatOpenid, CancellationToken cancellationToken = default); Task ExistsPhoneAsync(string phone, CancellationToken cancellationToken = default); diff --git a/src/Oms.Domain/KbAggregate/Repositories/IKbCategoryRepository.cs b/src/Oms.Domain/KbAggregate/Repositories/IKbCategoryRepository.cs index 4fe86d9..f10c44f 100644 --- a/src/Oms.Domain/KbAggregate/Repositories/IKbCategoryRepository.cs +++ b/src/Oms.Domain/KbAggregate/Repositories/IKbCategoryRepository.cs @@ -7,5 +7,7 @@ public interface IKbCategoryRepository : IRepository { Task GetByCodeAsync(string code, CancellationToken cancellationToken = default); + Task GetByCodeIncludingDeletedAsync(string code, CancellationToken cancellationToken = default); + Task ExistsCodeAsync(string code, int? excludeId = null, CancellationToken cancellationToken = default); } diff --git a/src/Oms.Domain/SystemAggregate/Repositories/IDictTypeRepository.cs b/src/Oms.Domain/SystemAggregate/Repositories/IDictTypeRepository.cs index fb0e07f..3b1a4aa 100644 --- a/src/Oms.Domain/SystemAggregate/Repositories/IDictTypeRepository.cs +++ b/src/Oms.Domain/SystemAggregate/Repositories/IDictTypeRepository.cs @@ -6,4 +6,11 @@ namespace Oms.Domain.SystemAggregate.Repositories; public interface IDictTypeRepository : IRepository { Task GetByDictTypeAsync(string dictType, CancellationToken cancellationToken = default); + + /// + /// 按类型编码查询(含软删除),用于唯一键冲突时恢复。 + /// + Task GetByDictTypeIncludingDeletedAsync( + string dictType, + CancellationToken cancellationToken = default); } diff --git a/src/Oms.Domain/SystemAggregate/Repositories/ISystemOtherConfigRepository.cs b/src/Oms.Domain/SystemAggregate/Repositories/ISystemOtherConfigRepository.cs index b1b3ffe..3b92cd8 100644 --- a/src/Oms.Domain/SystemAggregate/Repositories/ISystemOtherConfigRepository.cs +++ b/src/Oms.Domain/SystemAggregate/Repositories/ISystemOtherConfigRepository.cs @@ -6,4 +6,8 @@ namespace Oms.Domain.SystemAggregate.Repositories; public interface ISystemOtherConfigRepository : IRepository { Task GetByConfigCodeAsync(string configCode, CancellationToken cancellationToken = default); + + Task GetByConfigCodeIncludingDeletedAsync( + string configCode, + CancellationToken cancellationToken = default); } diff --git a/src/Oms.Domain/SystemAggregate/Repositories/IWarehouseAddressRepository.cs b/src/Oms.Domain/SystemAggregate/Repositories/IWarehouseAddressRepository.cs index 16bd587..131abb9 100644 --- a/src/Oms.Domain/SystemAggregate/Repositories/IWarehouseAddressRepository.cs +++ b/src/Oms.Domain/SystemAggregate/Repositories/IWarehouseAddressRepository.cs @@ -7,5 +7,9 @@ public interface IWarehouseAddressRepository : IRepository { Task GetByWarehouseNameAsync(string warehouseName, CancellationToken cancellationToken = default); + Task GetByWarehouseNameIncludingDeletedAsync( + string warehouseName, + CancellationToken cancellationToken = default); + Task ExistsWarehouseNameAsync(string warehouseName, CancellationToken cancellationToken = default); } diff --git a/src/Oms.Domain/WarehouseAggregate/Repositories/IWarehouseRepositories.cs b/src/Oms.Domain/WarehouseAggregate/Repositories/IWarehouseRepositories.cs index f2c0fb8..26c7073 100644 --- a/src/Oms.Domain/WarehouseAggregate/Repositories/IWarehouseRepositories.cs +++ b/src/Oms.Domain/WarehouseAggregate/Repositories/IWarehouseRepositories.cs @@ -3,9 +3,21 @@ using Oms.Domain.WarehouseAggregate.Entities; namespace Oms.Domain.WarehouseAggregate.Repositories; -public interface ICSkuRepository : IRepository; +public interface ICSkuRepository : IRepository +{ + Task GetByCustomerAndSkuCodeIncludingDeletedAsync( + int customerId, + string skuCode, + CancellationToken cancellationToken = default); +} -public interface ICBinLocationRepository : IRepository; +public interface ICBinLocationRepository : IRepository +{ + Task GetByWarehouseAndBinCodeIncludingDeletedAsync( + int warehouseId, + string binCode, + CancellationToken cancellationToken = default); +} public interface ICInventoryRepository : IRepository; diff --git a/src/Oms.SqlSugarCore/Ai/AiSkillRepository.cs b/src/Oms.SqlSugarCore/Ai/AiSkillRepository.cs index cac80c7..4204742 100644 --- a/src/Oms.SqlSugarCore/Ai/AiSkillRepository.cs +++ b/src/Oms.SqlSugarCore/Ai/AiSkillRepository.cs @@ -12,4 +12,15 @@ public class AiSkillRepository string skillKey, CancellationToken cancellationToken = default) => FirstOrDefaultAsync(x => x.SkillKey == skillKey, cancellationToken); + + public async Task GetBySkillKeyIncludingDeletedAsync( + string skillKey, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + return await SoftDeleteQuery.FirstIncludingDeletedAsync( + db, + x => x.SkillKey == skillKey, + cancellationToken); + } } diff --git a/src/Oms.SqlSugarCore/Channels/SalesProductRepository.cs b/src/Oms.SqlSugarCore/Channels/SalesProductRepository.cs index b6a1aab..fdc3af9 100644 --- a/src/Oms.SqlSugarCore/Channels/SalesProductRepository.cs +++ b/src/Oms.SqlSugarCore/Channels/SalesProductRepository.cs @@ -13,6 +13,17 @@ public class SalesProductRepository : OmsSqlSugarRepository, ISale return FirstOrDefaultAsync(x => x.ProductCode == productCode, cancellationToken); } + public async Task GetByProductCodeIncludingDeletedAsync( + string productCode, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + return await SoftDeleteQuery.FirstIncludingDeletedAsync( + db, + x => x.ProductCode == productCode, + cancellationToken); + } + public Task HasProductCodeAsync(string productCode, CancellationToken cancellationToken = default) { return AnyAsync(x => x.ProductCode == productCode, cancellationToken); diff --git a/src/Oms.SqlSugarCore/Channels/ServiceChannelRepository.cs b/src/Oms.SqlSugarCore/Channels/ServiceChannelRepository.cs index e7e80f2..c0c6e49 100644 --- a/src/Oms.SqlSugarCore/Channels/ServiceChannelRepository.cs +++ b/src/Oms.SqlSugarCore/Channels/ServiceChannelRepository.cs @@ -13,6 +13,17 @@ public class ServiceChannelRepository : OmsSqlSugarRepository, I return FirstOrDefaultAsync(x => x.ChannelCode == channelCode, cancellationToken); } + public async Task GetByChannelCodeIncludingDeletedAsync( + string channelCode, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + return await SoftDeleteQuery.FirstIncludingDeletedAsync( + db, + x => x.ChannelCode == channelCode, + cancellationToken); + } + public Task HasChannelCodeAsync(string channelCode, CancellationToken cancellationToken = default) { return AnyAsync(x => x.ChannelCode == channelCode, cancellationToken); diff --git a/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs b/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs index 22ed7bc..6215bd9 100644 --- a/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs +++ b/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs @@ -12,16 +12,49 @@ public class CustomerRepository : OmsSqlSugarRepository, ICustomerRepo return FirstOrDefaultAsync(x => x.CustomerCode == customerCode, cancellationToken); } + public async Task GetByCustomerCodeIncludingDeletedAsync( + string customerCode, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + return await SoftDeleteQuery.FirstIncludingDeletedAsync( + db, + x => x.CustomerCode == customerCode, + cancellationToken); + } + public Task GetByPhoneAsync(string phone, CancellationToken cancellationToken = default) { return FirstOrDefaultAsync(x => x.Phone == phone, cancellationToken); } + public async Task GetByPhoneIncludingDeletedAsync( + string phone, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + return await SoftDeleteQuery.FirstIncludingDeletedAsync( + db, + x => x.Phone == phone, + cancellationToken); + } + public Task GetByEmailAsync(string email, CancellationToken cancellationToken = default) { return FirstOrDefaultAsync(x => x.Email == email, cancellationToken); } + public async Task GetByEmailIncludingDeletedAsync( + string email, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + return await SoftDeleteQuery.FirstIncludingDeletedAsync( + db, + x => x.Email == email, + cancellationToken); + } + public Task GetByWechatOpenidAsync(string wechatOpenid, CancellationToken cancellationToken = default) { return FirstOrDefaultAsync(x => x.WechatOpenid == wechatOpenid, cancellationToken); diff --git a/src/Oms.SqlSugarCore/Kb/KbCategoryRepository.cs b/src/Oms.SqlSugarCore/Kb/KbCategoryRepository.cs index f6fdc90..0bd8fce 100644 --- a/src/Oms.SqlSugarCore/Kb/KbCategoryRepository.cs +++ b/src/Oms.SqlSugarCore/Kb/KbCategoryRepository.cs @@ -11,6 +11,17 @@ public class KbCategoryRepository public Task GetByCodeAsync(string code, CancellationToken cancellationToken = default) => FirstOrDefaultAsync(x => x.Code == code, cancellationToken); + public async Task GetByCodeIncludingDeletedAsync( + string code, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + return await SoftDeleteQuery.FirstIncludingDeletedAsync( + db, + x => x.Code == code, + cancellationToken); + } + public async Task ExistsCodeAsync( string code, int? excludeId = null, diff --git a/src/Oms.SqlSugarCore/Repositories/SoftDeleteQuery.cs b/src/Oms.SqlSugarCore/Repositories/SoftDeleteQuery.cs new file mode 100644 index 0000000..f77ab9a --- /dev/null +++ b/src/Oms.SqlSugarCore/Repositories/SoftDeleteQuery.cs @@ -0,0 +1,24 @@ +using System.Linq.Expressions; +using SqlSugar; + +namespace Oms.SqlSugarCore.Repositories; + +/// +/// 软删场景下按业务唯一键查询(ClearFilter),供新增恢复 / 释放唯一键使用。 +/// +public static class SoftDeleteQuery +{ + public static async Task FirstIncludingDeletedAsync( + ISqlSugarClient db, + Expression> predicate, + CancellationToken cancellationToken = default) + where TEntity : class, new() + { + var rows = await db.Queryable() + .ClearFilter() + .Where(predicate) + .Take(1) + .ToListAsync(cancellationToken); + return rows.FirstOrDefault(); + } +} diff --git a/src/Oms.SqlSugarCore/System/DictTypeRepository.cs b/src/Oms.SqlSugarCore/System/DictTypeRepository.cs index b52693e..e76441f 100644 --- a/src/Oms.SqlSugarCore/System/DictTypeRepository.cs +++ b/src/Oms.SqlSugarCore/System/DictTypeRepository.cs @@ -11,4 +11,17 @@ public class DictTypeRepository : OmsSqlSugarRepository, IDictType { return FirstOrDefaultAsync(x => x.DictType == dictType, cancellationToken); } + + public async Task GetByDictTypeIncludingDeletedAsync( + string dictType, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + var rows = await db.Queryable() + .ClearFilter() + .Where(x => x.DictType == dictType) + .Take(1) + .ToListAsync(cancellationToken); + return rows.FirstOrDefault(); + } } diff --git a/src/Oms.SqlSugarCore/System/SystemOtherConfigRepository.cs b/src/Oms.SqlSugarCore/System/SystemOtherConfigRepository.cs index e71892b..40bcfeb 100644 --- a/src/Oms.SqlSugarCore/System/SystemOtherConfigRepository.cs +++ b/src/Oms.SqlSugarCore/System/SystemOtherConfigRepository.cs @@ -11,4 +11,15 @@ public class SystemOtherConfigRepository : OmsSqlSugarRepository x.ConfigCode == configCode, cancellationToken); } + + public async Task GetByConfigCodeIncludingDeletedAsync( + string configCode, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + return await SoftDeleteQuery.FirstIncludingDeletedAsync( + db, + x => x.ConfigCode == configCode, + cancellationToken); + } } diff --git a/src/Oms.SqlSugarCore/System/WarehouseAddressRepository.cs b/src/Oms.SqlSugarCore/System/WarehouseAddressRepository.cs index e6233c5..40a7eb1 100644 --- a/src/Oms.SqlSugarCore/System/WarehouseAddressRepository.cs +++ b/src/Oms.SqlSugarCore/System/WarehouseAddressRepository.cs @@ -12,6 +12,17 @@ public class WarehouseAddressRepository : OmsSqlSugarRepository x.WarehouseName == warehouseName, cancellationToken); } + public async Task GetByWarehouseNameIncludingDeletedAsync( + string warehouseName, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + return await SoftDeleteQuery.FirstIncludingDeletedAsync( + db, + x => x.WarehouseName == warehouseName, + cancellationToken); + } + public Task ExistsWarehouseNameAsync(string warehouseName, CancellationToken cancellationToken = default) { return AnyAsync(x => x.WarehouseName == warehouseName, cancellationToken); diff --git a/src/Oms.SqlSugarCore/Warehouse/WarehouseRepositories.cs b/src/Oms.SqlSugarCore/Warehouse/WarehouseRepositories.cs index c9e6193..08ecd97 100644 --- a/src/Oms.SqlSugarCore/Warehouse/WarehouseRepositories.cs +++ b/src/Oms.SqlSugarCore/Warehouse/WarehouseRepositories.cs @@ -5,9 +5,35 @@ using Oms.SqlSugarCore.Repositories; namespace Oms.SqlSugarCore.Warehouse; -public class CSkuRepository : OmsSqlSugarRepository, ICSkuRepository, IScopedDependency; +public class CSkuRepository : OmsSqlSugarRepository, ICSkuRepository, IScopedDependency +{ + public async Task GetByCustomerAndSkuCodeIncludingDeletedAsync( + int customerId, + string skuCode, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + return await SoftDeleteQuery.FirstIncludingDeletedAsync( + db, + x => x.CustomerId == customerId && x.SkuCode == skuCode, + cancellationToken); + } +} -public class CBinLocationRepository : OmsSqlSugarRepository, ICBinLocationRepository, IScopedDependency; +public class CBinLocationRepository : OmsSqlSugarRepository, ICBinLocationRepository, IScopedDependency +{ + public async Task GetByWarehouseAndBinCodeIncludingDeletedAsync( + int warehouseId, + string binCode, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + return await SoftDeleteQuery.FirstIncludingDeletedAsync( + db, + x => x.WarehouseId == warehouseId && x.BinCode == binCode, + cancellationToken); + } +} public class CInventoryRepository : OmsSqlSugarRepository, ICInventoryRepository, IScopedDependency;