diff --git a/Sln.Wcs.Business.Tests/Sln.Wcs.Business.Tests.csproj b/Sln.Wcs.Business.Tests/Sln.Wcs.Business.Tests.csproj new file mode 100644 index 0000000..63712b7 --- /dev/null +++ b/Sln.Wcs.Business.Tests/Sln.Wcs.Business.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Sln.Wcs.Business.Tests/StoreTaskBusinessTests.cs b/Sln.Wcs.Business.Tests/StoreTaskBusinessTests.cs new file mode 100644 index 0000000..9ff1ed1 --- /dev/null +++ b/Sln.Wcs.Business.Tests/StoreTaskBusinessTests.cs @@ -0,0 +1,515 @@ +#region << 版 本 注 释 >> +/*-------------------------------------------------------------------- +|* 版权所有 (c) 2026 WenJY 保留所有权利。 +|* CLR版本:4.0.30319.42000 +|* 机器名称:Mr.Wen's MacBook Pro +|* 命名空间:Sln.Wcs.Business.Tests +|* 唯一标识: +|* +|* 创建者:WenJY +|* 电子邮箱: +|* 创建时间:2026-05-17 +|* 版本:V1.0.0 +|* 描述:StoreTaskBusiness 单元测试 +|* +|*-------------------------------------------------------------------- +|* 修改人: +|* 时间: +|* 修改说明: +|* +|* 版本:V1.0.0 +|*--------------------------------------------------------------------*/ +#endregion << 版 本 注 释 >> + +using Moq; +using Sln.Wcs.Business.Domain.Dto.CreateTask; +using Sln.Wcs.Business.Domain.Dto.FilterLocation; +using Sln.Wcs.Business.Domain.Dto.SaveTask; +using Sln.Wcs.Business.Domain.Enum; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; +using Xunit; + +namespace Sln.Wcs.Business.Tests; + +/// +/// 可测试的 StoreTaskBusiness 子类,用于访问 protected 方法 +/// +public class TestableStoreTaskBusiness : StoreTaskBusiness +{ + public TestableStoreTaskBusiness( + IBasePathInfoService basePathInfoService, + ILiveTaskQueueService liveTaskQueueService, + IBaseStoreInfoService baseStoreInfoService) + : base(basePathInfoService, liveTaskQueueService, baseStoreInfoService) + { + } + + public void PublicValidateCreateTaskParams(CreateTaskDto dto) => + ValidateCreateTaskParams(dto); + + public void PublicValidateFilterLocationParams(FilterLocationDto dto) => + ValidateFilterLocationParams(dto); + + public string PublicGenerateTaskCode() => GenerateTaskCode(); + + public string PublicGetCategoryName(TaskCategoryEnum category) => + GetCategoryName(category); + + public string PublicGetTaskTypeName(TaskTypeEnum taskType) => + GetTaskTypeName(taskType); + + public string PublicGetTaskSuccessMessage( + string taskCode, string pathName, TaskTypeEnum taskType, TaskCategoryEnum taskCategory) => + GetTaskSuccessMessage(taskCode, pathName, taskType, taskCategory); + + public BasePathInfo PublicGetPathInfo( + CreateTaskDto createTaskDto, TaskTypeEnum taskType, TaskCategoryEnum taskCategory) => + GetPathInfo(createTaskDto, taskType, taskCategory); +} + +public class StoreTaskBusinessTests +{ + private readonly Mock _mockBasePathInfoService; + private readonly Mock _mockLiveTaskQueueService; + private readonly Mock _mockBaseStoreInfoService; + private readonly TestableStoreTaskBusiness _storeTaskBusiness; + + public StoreTaskBusinessTests() + { + _mockBasePathInfoService = new Mock(); + _mockLiveTaskQueueService = new Mock(); + _mockBaseStoreInfoService = new Mock(); + _storeTaskBusiness = new TestableStoreTaskBusiness( + _mockBasePathInfoService.Object, + _mockLiveTaskQueueService.Object, + _mockBaseStoreInfoService.Object); + } + + #region GetCategoryName 测试 + + [Theory] + [InlineData(TaskCategoryEnum.Material, "包材")] + [InlineData(TaskCategoryEnum.Product, "成品")] + [InlineData(TaskCategoryEnum.Pallet, "托盘")] + public void GetCategoryName_ValidCategory_ReturnsCorrectName(TaskCategoryEnum category, string expected) + { + var result = _storeTaskBusiness.PublicGetCategoryName(category); + Assert.Equal(expected, result); + } + + #endregion + + #region GetTaskTypeName 测试 + + [Theory] + [InlineData(TaskTypeEnum.InStore, "入库")] + [InlineData(TaskTypeEnum.OutStore, "出库")] + public void GetTaskTypeName_ValidType_ReturnsCorrectName(TaskTypeEnum taskType, string expected) + { + var result = _storeTaskBusiness.PublicGetTaskTypeName(taskType); + Assert.Equal(expected, result); + } + + #endregion + + #region ValidateCreateTaskParams 测试 + + [Fact] + public void ValidateCreateTaskParams_NullMaterialCode_ThrowsException() + { + var dto = new CreateTaskDto + { + materialCode = null!, + palletBarcode = "PLT001", + startPoint = "A", + endPoint = "B" + }; + + var exception = Assert.Throws(() => + _storeTaskBusiness.PublicValidateCreateTaskParams(dto)); + + Assert.Contains("物料编号不允许为 NULL", exception.Message); + } + + [Fact] + public void ValidateCreateTaskParams_NullPalletBarcode_ThrowsException() + { + var dto = new CreateTaskDto + { + materialCode = "MAT001", + palletBarcode = null!, + startPoint = "A", + endPoint = "B", + taskCategory = TaskCategoryEnum.Material + }; + + var exception = Assert.Throws(() => + _storeTaskBusiness.PublicValidateCreateTaskParams(dto)); + + Assert.Contains("条码不允许为 NULL", exception.Message); + } + + [Fact] + public void ValidateCreateTaskParams_NullStartPoint_ThrowsException() + { + var dto = new CreateTaskDto + { + materialCode = "MAT001", + palletBarcode = "PLT001", + startPoint = null!, + endPoint = "B" + }; + + var exception = Assert.Throws(() => + _storeTaskBusiness.PublicValidateCreateTaskParams(dto)); + + Assert.Contains("起始位置、终点位置不允许为 NULL", exception.Message); + } + + [Fact] + public void ValidateCreateTaskParams_EmptyEndPoint_ThrowsException() + { + var dto = new CreateTaskDto + { + materialCode = "MAT001", + palletBarcode = "PLT001", + startPoint = "A", + endPoint = "" + }; + + var exception = Assert.Throws(() => + _storeTaskBusiness.PublicValidateCreateTaskParams(dto)); + + Assert.Contains("起始位置、终点位置不允许为 NULL", exception.Message); + } + + [Fact] + public void ValidateCreateTaskParams_ValidParams_NoException() + { + var dto = new CreateTaskDto + { + materialCode = "MAT001", + palletBarcode = "PLT001", + startPoint = "A", + endPoint = "B", + taskCategory = TaskCategoryEnum.Material + }; + + var exception = Record.Exception(() => + _storeTaskBusiness.PublicValidateCreateTaskParams(dto)); + + Assert.Null(exception); + } + + #endregion + + #region ValidateFilterLocationParams 测试 + + [Fact] + public void ValidateFilterLocationParams_NullMaterialCode_ThrowsException() + { + var dto = new FilterLocationDto + { + materialCode = null! + }; + + var exception = Assert.Throws(() => + _storeTaskBusiness.PublicValidateFilterLocationParams(dto)); + + Assert.Contains("物料编号不允许为 NULL", exception.Message); + } + + [Fact] + public void ValidateFilterLocationParams_ValidParams_NoException() + { + var dto = new FilterLocationDto + { + materialCode = "MAT001" + }; + + var exception = Record.Exception(() => + _storeTaskBusiness.PublicValidateFilterLocationParams(dto)); + + Assert.Null(exception); + } + + #endregion + + #region GenerateTaskCode 测试 + + [Fact] + public void GenerateTaskCode_ReturnsCorrectFormat() + { + var taskCode = _storeTaskBusiness.PublicGenerateTaskCode(); + + Assert.NotNull(taskCode); + Assert.Equal(18, taskCode.Length); + Assert.True(long.TryParse(taskCode[..14], out _)); + Assert.True(int.TryParse(taskCode[14..], out var randomPart)); + Assert.InRange(randomPart, 1000, 9999); + } + + [Fact] + public void GenerateTaskCode_MultipleCalls_GeneratesUniqueCodes() + { + var codes = new HashSet(); + for (int i = 0; i < 100; i++) + { + codes.Add(_storeTaskBusiness.PublicGenerateTaskCode()); + } + + Assert.Equal(100, codes.Count); + } + + #endregion + + #region CreateTask 测试 + + [Fact] + public void CreateTask_ValidInput_ReturnsSuccessResult() + { + var dto = new CreateTaskDto + { + materialCode = "MAT001", + materialBarcode = "MAT001BAR", + palletBarcode = "PLT001", + amount = 100, + startPoint = "START", + endPoint = "END", + taskType = TaskTypeEnum.InStore, + taskCategory = TaskCategoryEnum.Material + }; + + var pathDetails = new List + { + new BasePathDetails + { + objId = 1, + pathCode = "PATH1", + startPoint = "START", + endPoint = "MID", + deviceType = 0 + }, + new BasePathDetails + { + objId = 2, + pathCode = "PATH2", + startPoint = "MID", + endPoint = "END", + deviceType = 0 + } + }; + + var pathInfo = new BasePathInfo + { + objId = 1, + pathCode = "MAIN_PATH", + pathName = "主输送线", + pathType = 1, + pathCategory = 1, + startPoint = "START", + endPoint = "END", + pathDetails = pathDetails + }; + + _mockBasePathInfoService + .Setup(x => x.GetBasePathInfo(It.IsAny>>())) + .Returns(new List { pathInfo }); + + var result = _storeTaskBusiness.CreateTask(dto); + + Assert.Equal(BusinessStatusEnum.成功, result.code); + Assert.NotNull(result.data); + Assert.NotNull(result.data.taskCode); + Assert.Equal(2, result.data.taskDetails.Count); + Assert.Equal(2, result.data.taskQueue.taskSteps); + } + + [Fact] + public void CreateTask_PathNotFound_ReturnsErrorResult() + { + var dto = new CreateTaskDto + { + materialCode = "MAT001", + palletBarcode = "PLT001", + startPoint = "INVALID", + endPoint = "INVALID", + taskType = TaskTypeEnum.InStore, + taskCategory = TaskCategoryEnum.Material + }; + + _mockBasePathInfoService + .Setup(x => x.GetBasePathInfo(It.IsAny>>())) + .Returns(new List()); + + var result = _storeTaskBusiness.CreateTask(dto); + + Assert.Equal(BusinessStatusEnum.方法执行异常, result.code); + Assert.Contains("输送路径为 NULL", result.msg); + } + + #endregion + + #region FilterLocation 测试 + + [Fact] + public void FilterLocation_ValidInput_ReturnsSuccessResult() + { + var dto = new FilterLocationDto + { + materialCode = "MAT001", + taskType = TaskTypeEnum.InStore, + taskCategory = TaskCategoryEnum.Material + }; + + var locationInfos = new List + { + new BaseLocationInfo + { + locationCode = "LOC001", + locationRows = 1, + locationColumns = 1, + locationLayers = 1, + locationStatus = 0, + materialCode = "MAT001", + isFlag = 1 + } + }; + + var storeInfo = new BaseStoreInfo + { + storeCode = "STORE001", + storeName = "测试仓库", + storeType = 1, + isFlag = 1, + locationInfos = locationInfos + }; + + _mockBaseStoreInfoService + .Setup(x => x.GetBasePathInfo( + It.IsAny>>(), + It.IsAny>>())) + .Returns(new List { storeInfo }); + + var result = _storeTaskBusiness.FilterLocation(dto); + + Assert.Equal(BusinessStatusEnum.成功, result.code); + Assert.NotNull(result.data); + Assert.NotNull(result.data.storeInfo); + Assert.NotNull(result.data.locationInfo); + Assert.Single(result.data.locationInfos); + } + + [Fact] + public void FilterLocation_NoAvailableStore_ReturnsErrorResult() + { + var dto = new FilterLocationDto + { + materialCode = "MAT001", + taskType = TaskTypeEnum.InStore, + taskCategory = TaskCategoryEnum.Material + }; + + _mockBaseStoreInfoService + .Setup(x => x.GetBasePathInfo( + It.IsAny>>(), + It.IsAny>>())) + .Returns(new List()); + + var result = _storeTaskBusiness.FilterLocation(dto); + + Assert.Equal(BusinessStatusEnum.方法执行异常, result.code); + Assert.Contains("未获取到可用仓库", result.msg); + } + + #endregion + + #region SaveTask 测试 + + [Fact] + public void SaveTask_ValidInput_ReturnsSuccessResult() + { + var dto = new SaveTaskDto + { + taskQueue = new LiveTaskQueue + { + taskCode = "TASK001", + materialCode = "MAT001", + palletBarcode = "PLT001", + startPoint = "A", + endPoint = "B", + taskType = 1, + taskCategory = 1 + } + }; + + _mockLiveTaskQueueService + .Setup(x => x.InsertTaskQueue(It.IsAny())) + .Returns(true); + + var result = _storeTaskBusiness.SaveTask(dto); + + Assert.Equal(BusinessStatusEnum.成功, result.code); + Assert.NotNull(result.data); + Assert.True(result.data.isRes); + } + + [Fact] + public void SaveTask_InsertFails_ReturnsFalse() + { + var dto = new SaveTaskDto + { + taskQueue = new LiveTaskQueue + { + taskCode = "TASK001", + materialCode = "MAT001", + palletBarcode = "PLT001", + startPoint = "A", + endPoint = "B", + taskType = 1, + taskCategory = 1 + } + }; + + _mockLiveTaskQueueService + .Setup(x => x.InsertTaskQueue(It.IsAny())) + .Returns(false); + + var result = _storeTaskBusiness.SaveTask(dto); + + Assert.Equal(BusinessStatusEnum.成功, result.code); + Assert.NotNull(result.data); + Assert.False(result.data.isRes); + } + + #endregion + + #region GetTaskSuccessMessage 测试 + + [Fact] + public void GetTaskSuccessMessage_ReturnsCorrectMessage() + { + var message = _storeTaskBusiness.PublicGetTaskSuccessMessage( + "TASK001", + "主输送线", + TaskTypeEnum.InStore, + TaskCategoryEnum.Material); + + Assert.Equal("包材入库任务创建成功:TASK001;关联路径:主输送线", message); + } + + [Fact] + public void GetTaskSuccessMessage_ProductOutStore_ReturnsCorrectMessage() + { + var message = _storeTaskBusiness.PublicGetTaskSuccessMessage( + "TASK002", + "成品出库线", + TaskTypeEnum.OutStore, + TaskCategoryEnum.Product); + + Assert.Equal("成品出库任务创建成功:TASK002;关联路径:成品出库线", message); + } + + #endregion +} diff --git a/Sln.Wcs.Business/Domain/Dto/FilterLocation/FilterLocationDto.cs b/Sln.Wcs.Business/Domain/Dto/FilterLocation/FilterLocationDto.cs index 9ed07ac..699351a 100644 --- a/Sln.Wcs.Business/Domain/Dto/FilterLocation/FilterLocationDto.cs +++ b/Sln.Wcs.Business/Domain/Dto/FilterLocation/FilterLocationDto.cs @@ -23,6 +23,8 @@ #endregion << 版 本 注 释 >> +using Sln.Wcs.Business.Domain.Enum; + namespace Sln.Wcs.Business.Domain.Dto.FilterLocation; public class FilterLocationDto @@ -46,4 +48,14 @@ public class FilterLocationDto /// 数量 /// public string amount {get;set;} + + /// + /// 任务类型:1-入库;2-出库; + /// + public TaskTypeEnum taskType { get; set; } + + /// + /// 任务类别:1-包材;2-成品;3-托盘 + /// + public TaskCategoryEnum taskCategory { get; set; } } \ No newline at end of file diff --git a/Sln.Wcs.Business/StoreTaskBusiness.cs b/Sln.Wcs.Business/StoreTaskBusiness.cs new file mode 100644 index 0000000..366aa73 --- /dev/null +++ b/Sln.Wcs.Business/StoreTaskBusiness.cs @@ -0,0 +1,316 @@ +#region << 版 本 注 释 >> + +/*-------------------------------------------------------------------- +|* 版权所有 (c) 2026 WenJY 保留所有权利。 +|* CLR版本:4.0.30319.42000 +|* 机器名称:Mr.Wen's MacBook Pro +|* 命名空间:Sln.Wcs.Business +|* 唯一标识:7E8A9B2C-3D4F-5E6A-7B8C-9D0E1F2A3B4C +|* +|* 创建者:WenJY +|* 电子邮箱: +|* 创建时间:2026-05-15 15:30:00 +|* 版本:V1.0.0 +|* 描述:通用仓储任务业务处理类,支持入库/出库,包材/成品/托盘分类 +|* +|*-------------------------------------------------------------------- +|* 修改人: +|* 时间: +|* 修改说明: +|* +|* 版本:V1.0.0 +|*--------------------------------------------------------------------*/ + +#endregion << 版 本 注 释 >> + +using System.Linq.Expressions; +using Sln.Wcs.Business.Domain.Dto.CreateTask; +using Sln.Wcs.Business.Domain.Dto.FilterLocation; +using Sln.Wcs.Business.Domain.Dto.SaveTask; +using Sln.Wcs.Business.Domain.Dto.ValidateMaterial; +using Sln.Wcs.Business.Domain.Enum; +using Sln.Wcs.Business.Domain.Model.CreateTask; +using Sln.Wcs.Business.Domain.Model.FilterLocation; +using Sln.Wcs.Business.Domain.Model.SaveTask; +using Sln.Wcs.Business.Util; +using Sln.Wcs.Model.Domain; +using Sln.Wcs.Repository.service; + +namespace Sln.Wcs.Business; + +/// +/// 通用仓储任务业务处理类 +/// 支持扩展点:子类可重写虚方法实现特定业务逻辑 +/// +public class StoreTaskBusiness : EntityWrapper +{ + protected readonly IBasePathInfoService _basePathInfoService; + protected readonly ILiveTaskQueueService _liveTaskQueueService; + protected readonly IBaseStoreInfoService _baseStoreInfoService; + + /// + /// 构造函数 + /// + /// 路径服务 + /// 任务队列服务 + /// 仓库服务 + public StoreTaskBusiness( + IBasePathInfoService basePathInfoService, + ILiveTaskQueueService liveTaskQueueService, + IBaseStoreInfoService baseStoreInfoService) + { + _basePathInfoService = basePathInfoService; + _liveTaskQueueService = liveTaskQueueService; + _baseStoreInfoService = baseStoreInfoService; + } + + #region 虚方法 - 供子类重写的扩展点 + + /// + /// 校验物料(可被子类重写) + /// + public virtual ValidateMaterialResultDto ValidateMaterial(ValidateMaterialDto validateMaterialDto) + { + throw new NotImplementedException(); + } + + /// + /// 创建任务(可被子类重写) + /// + /// 创建任务参数(包含taskType和taskCategory) + public virtual CreateTaskResultDto CreateTask(CreateTaskDto createTaskDto) + { + CreateTaskResultDto resultDto = new CreateTaskResultDto(); + try + { + // 1. 参数校验(可扩展) + ValidateCreateTaskParams(createTaskDto); + + // 2. 获取输送路径 + BasePathInfo pathInfo = GetPathInfo(createTaskDto, createTaskDto.taskType, createTaskDto.taskCategory); + + // 4. 生成任务编号 + string taskCode = GenerateTaskCode(); + + // 5. 路径转为任务 + List taskDetails = pathInfo.pathDetails + .Select(item => LiveTaskDetailWrapper(taskCode, createTaskDto, item)) + .ToList(); + + var taskQueue = LiveTaskQueueWrapper(taskCode, createTaskDto, pathInfo); + taskQueue.taskSteps = taskDetails.Count; + taskQueue.taskDetails = taskDetails; + + // 6. 返回结果 + resultDto.code = Domain.Enum.BusinessStatusEnum.成功; + resultDto.msg = GetTaskSuccessMessage(taskCode, pathInfo.pathName, createTaskDto.taskType, createTaskDto.taskCategory); + resultDto.data = new CreateTaskResultModel() + { + taskCode = taskCode, + taskQueue = taskQueue, + taskDetails = taskDetails, + }; + } + catch (Exception e) + { + resultDto.code = Domain.Enum.BusinessStatusEnum.方法执行异常; + resultDto.msg = e.Message; + } + return resultDto; + } + + /// + /// 筛选库位(可被子类重写) + /// + /// 筛选库位参数(包含taskType和taskCategory) + public virtual FilterLocationResultDto FilterLocation(FilterLocationDto filterLocationDto) + { + FilterLocationResultDto resultDto = new FilterLocationResultDto(); + try + { + // 1. 参数校验(可扩展) + ValidateFilterLocationParams(filterLocationDto); + + // 2. 获取库位状态(入库:未使用=0,出库:已使用=1) + int locationStatus = filterLocationDto.taskType == TaskTypeEnum.InStore ? 0 : 1; + + // 3. 构建查询条件 + Expression> storeWhere = GetStoreWhere(filterLocationDto.taskType, filterLocationDto.taskCategory); + Expression> locationWhere = GetLocationWhere(filterLocationDto, locationStatus); + + // 4. 查询仓库和库位 + List storeInfos = _baseStoreInfoService.GetBasePathInfo(storeWhere, locationWhere); + + // 5. 选择最优仓库和库位 + BaseStoreInfo? storeInfo = storeInfos + .Where(s => s.locationInfos.Count > 0) + .OrderBy(x => x.storeCode) + .FirstOrDefault() ?? throw new ArgumentNullException($"未获取到可用仓库"); + + BaseLocationInfo? locationInfo = storeInfo.locationInfos + .OrderBy(x => x.locationRows) + .ThenBy(x => x.locationColumns) + .ThenBy(x => x.locationLayers) + .FirstOrDefault() ?? throw new ArgumentNullException($"目标仓库:{storeInfo.storeName}中未获取到可用库位"); + + resultDto.code = Domain.Enum.BusinessStatusEnum.成功; + resultDto.msg = "执行完成"; + resultDto.data = new FilterLocationResultModel() + { + storeInfo = storeInfo, + locationInfos = storeInfo.locationInfos, + locationInfo = locationInfo, + }; + } + catch (Exception e) + { + resultDto.code = Domain.Enum.BusinessStatusEnum.方法执行异常; + resultDto.msg = e.Message; + } + return resultDto; + } + + /// + /// 保存任务(可被子类重写) + /// + /// 保存任务参数 + public virtual SaveTaskResultDto SaveTask(SaveTaskDto saveTaskDto) + { + SaveTaskResultDto resultDto = new SaveTaskResultDto(); + try + { + var inRes = _liveTaskQueueService.InsertTaskQueue(saveTaskDto.taskQueue); + + resultDto.code = Domain.Enum.BusinessStatusEnum.成功; + resultDto.msg = "执行完成"; + resultDto.data = new SaveTaskResultModel() + { + isRes = inRes + }; + } + catch (Exception e) + { + resultDto.code = Domain.Enum.BusinessStatusEnum.方法执行异常; + resultDto.msg = e.Message; + } + return resultDto; + } + + #endregion + + #region 受保护虚方法 - 可被子类定制 + + /// + /// 校验创建任务参数(可被子类重写添加额外校验) + /// + protected virtual void ValidateCreateTaskParams(CreateTaskDto createTaskDto) + { + if (string.IsNullOrEmpty(createTaskDto.materialCode)) + { + throw new InvalidOperationException($"物料编号不允许为 NULL"); + } + + if (string.IsNullOrEmpty(createTaskDto.palletBarcode)) + { + throw new InvalidOperationException($"{GetCategoryName(createTaskDto.taskCategory)}条码不允许为 NULL"); + } + + if (string.IsNullOrEmpty(createTaskDto.startPoint) || string.IsNullOrEmpty(createTaskDto.endPoint)) + { + throw new InvalidOperationException($"起始位置、终点位置不允许为 NULL"); + } + } + + /// + /// 校验筛选库位参数(可被子类重写添加额外校验) + /// + protected virtual void ValidateFilterLocationParams(FilterLocationDto filterLocationDto) + { + if (string.IsNullOrEmpty(filterLocationDto.materialCode)) + { + throw new InvalidOperationException($"物料编号不允许为 NULL"); + } + } + + /// + /// 获取路径信息(可被子类重写定制路径查询逻辑) + /// + protected virtual BasePathInfo GetPathInfo(CreateTaskDto createTaskDto, TaskTypeEnum taskType, TaskCategoryEnum taskCategory) + { + Expression> exp = x => + x.startPoint == createTaskDto.startPoint && + x.endPoint == createTaskDto.endPoint && + x.pathType == (int)taskType && + x.pathCategory == (int)taskCategory; + + return _basePathInfoService.GetBasePathInfo(exp) + .FirstOrDefault() ?? throw new InvalidOperationException($"{GetCategoryName(taskCategory)}{GetTaskTypeName(taskType)}输送路径为 NULL"); + } + + /// + /// 生成任务编号(可被子类重写定制生成规则) + /// + protected virtual string GenerateTaskCode() + { + return DateTime.Now.ToString("yyyyMMddHHmmss") + new Random().Next(1000, 9999); + } + + /// + /// 获取任务成功的消息(可被子类重写) + /// + protected virtual string GetTaskSuccessMessage(string taskCode, string pathName, TaskTypeEnum taskType, TaskCategoryEnum taskCategory) + { + return $"{GetCategoryName(taskCategory)}{GetTaskTypeName(taskType)}任务创建成功:{taskCode};关联路径:{pathName}"; + } + + /// + /// 获取仓库查询条件(可被子类重写) + /// + protected virtual Expression> GetStoreWhere(TaskTypeEnum taskType, TaskCategoryEnum taskCategory) + { + return x => x.storeType == (int)taskCategory && x.isFlag == 1; + } + + /// + /// 获取库位查询条件(可被子类重写) + /// + protected virtual Expression> GetLocationWhere(FilterLocationDto filterLocationDto, int locationStatus) + { + return x => x.materialCode == filterLocationDto.materialCode && + x.locationStatus == locationStatus && + x.isFlag == 1; + } + + #endregion + + #region 辅助方法 + + /// + /// 获取分类名称 + /// + protected string GetCategoryName(TaskCategoryEnum taskCategory) + { + return taskCategory switch + { + TaskCategoryEnum.Material => "包材", + TaskCategoryEnum.Product => "成品", + TaskCategoryEnum.Pallet => "托盘", + _ => "未知" + }; + } + + /// + /// 获取任务类型名称 + /// + protected string GetTaskTypeName(TaskTypeEnum taskType) + { + return taskType switch + { + TaskTypeEnum.InStore => "入库", + TaskTypeEnum.OutStore => "出库", + _ => "未知" + }; + } + + #endregion +} diff --git a/Sln.Wcs.Business/StoreTaskBusinessEx.cs b/Sln.Wcs.Business/StoreTaskBusinessEx.cs new file mode 100644 index 0000000..ffa6b83 --- /dev/null +++ b/Sln.Wcs.Business/StoreTaskBusinessEx.cs @@ -0,0 +1,156 @@ +#region << 版 本 注 释 >> + +/*-------------------------------------------------------------------- +|* 版权所有 (c) 2026 WenJY 保留所有权利。 +|* CLR版本:4.0.30319.42000 +|* 机器名称:Mr.Wen's MacBook Pro +|* 命名空间:Sln.Wcs.Business +|* 唯一标识:9A0B1C2D-3E4F-5A6B-7C8D-9E0F1A2B3C4D +|* +|* 创建者:WenJY +|* 电子邮箱: +|* 创建时间:2026-05-15 15:40:00 +|* 版本:V1.0.0 +|* 描述:通用仓储任务业务扩展示例 +|* 展示如何继承 StoreTaskBusiness 定制特定业务逻辑 +|* 实际使用时,复制此类并修改为具体业务名称 +|* +|*-------------------------------------------------------------------- +|* 修改人: +|* 时间: +|* 修改说明: +|* +|* 版本:V1.0.0 +|*--------------------------------------------------------------------*/ + +#endregion << 版 本 注 释 >> + +using Sln.Wcs.Business.Domain.Dto.CreateTask; +using Sln.Wcs.Business.Domain.Dto.FilterLocation; +using Sln.Wcs.Business.Domain.Dto.ValidateMaterial; +using Sln.Wcs.Business.Domain.Enum; +using Sln.Wcs.Repository.service; + +namespace Sln.Wcs.Business; + +/// +/// 通用仓储任务业务扩展基类 +/// 当某个业务场景需要定制特殊逻辑时,继承此类 +/// +/// 物料校验DTO类型 +public abstract class StoreTaskBusinessEx : StoreTaskBusiness + where TValidateMaterialDto : ValidateMaterialDto +{ + protected StoreTaskBusinessEx( + IBasePathInfoService basePathInfoService, + ILiveTaskQueueService liveTaskQueueService, + IBaseStoreInfoService baseStoreInfoService) + : base(basePathInfoService, liveTaskQueueService, baseStoreInfoService) + { + } + + /// + /// 重写物料校验(示例) + /// + public override ValidateMaterialResultDto ValidateMaterial(ValidateMaterialDto validateMaterialDto) + { + // 调用子类自定义的校验逻辑 + return ValidateMaterialCore((TValidateMaterialDto)validateMaterialDto); + } + + /// + /// 子类实现具体的物料校验逻辑 + /// + protected abstract ValidateMaterialResultDto ValidateMaterialCore(TValidateMaterialDto validateMaterialDto); +} + +/// +/// ============================================================= +/// 以下为具体业务扩展示例(使用时取消注释并修改) +/// ============================================================= + + +// ===== 示例1:包材入库特殊业务 ===== +//public class MaterialInStoreEx : StoreTaskBusiness +//{ +// public MaterialInStoreEx( +// IBasePathInfoService basePathInfoService, +// ILiveTaskQueueService liveTaskQueueService, +// IBaseStoreInfoService baseStoreInfoService) +// : base(basePathInfoService, liveTaskQueueService, baseStoreInfoService) +// { +// } +// +// /// +// /// 重写任务编号生成规则(包材特殊规则) +// /// +// protected override string GenerateTaskCode() +// { +// // 示例:包材入库任务编号以 "M" 开头 +// return "M" + DateTime.Now.ToString("yyyyMMddHHmmss") + new Random().Next(100, 999); +// } +// +// /// +// /// 重写入库成功的消息 +// /// +// protected override string GetTaskSuccessMessage(string taskCode, string pathName, TaskTypeEnum taskType, TaskCategoryEnum taskCategory) +// { +// return $"包材入库任务创建成功(特殊规则):{taskCode};关联路径:{pathName}"; +// } +//} + +// ===== 示例2:成品出库特殊业务 ===== +//public class ProductOutStoreEx : StoreTaskBusiness +//{ +// public ProductOutStoreEx( +// IBasePathInfoService basePathInfoService, +// ILiveTaskQueueService liveTaskQueueService, +// IBaseStoreInfoService baseStoreInfoService) +// : base(basePathInfoService, liveTaskQueueService, baseStoreInfoService) +// { +// } +// +// /// +// /// 重写库位查询条件(成品出库需要特殊校验) +// /// +// protected override Expression> GetLocationWhere(FilterLocationDto filterLocationDto, int locationStatus) +// { +// // 成品出库可能有额外的有效期校验等 +// return x => x.materialCode == filterLocationDto.materialCode && +// x.locationStatus == locationStatus && +// x.isFlag == 1 && +// x.remark != "冻结"; // 排除冻结库位 +// } +// +// /// +// /// 重写任务编号生成规则(成品特殊规则) +// /// +// protected override string GenerateTaskCode() +// { +// // 示例:成品出库任务编号以 "P" 开头 +// return "P" + DateTime.Now.ToString("yyyyMMddHHmmss") + new Random().Next(100, 999); +// } +//} + +// ===== 示例3:托盘入库特殊业务 ===== +//public class PalletInStoreEx : StoreTaskBusiness +//{ +// public PalletInStoreEx( +// IBasePathInfoService basePathInfoService, +// ILiveTaskQueueService liveTaskQueueService, +// IBaseStoreInfoService baseStoreInfoService) +// : base(basePathInfoService, liveTaskQueueService, baseStoreInfoService) +// { +// } +// +// /// +// /// 重写创建任务参数校验(托盘可能有重量校验) +// /// +// protected override void ValidateCreateTaskParams(CreateTaskDto createTaskDto) +// { +// base.ValidateCreateTaskParams(createTaskDto); +// +// // 添加托盘特有的校验逻辑 +// // if (createTaskDto.Weight > 1000) throw new InvalidOperationException("托盘重量超限"); +// } +//} diff --git a/Sln.Wcs.sln b/Sln.Wcs.sln index a7bd297..67a0951 100644 --- a/Sln.Wcs.sln +++ b/Sln.Wcs.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.10.35122.118 MinimumVisualStudioVersion = 10.0.40219.1 @@ -28,6 +28,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.Business", "Sln.Wcs EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.Strategy", "Sln.Wcs.Strategy\Sln.Wcs.Strategy.csproj", "{F7658F97-F78A-4612-A1A5-490F2CDE49DD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.Business.Tests", "Sln.Wcs.Business.Tests\Sln.Wcs.Business.Tests.csproj", "{0911EA85-F152-453E-BB7D-4C7079361443}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,6 +88,10 @@ Global {F7658F97-F78A-4612-A1A5-490F2CDE49DD}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7658F97-F78A-4612-A1A5-490F2CDE49DD}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7658F97-F78A-4612-A1A5-490F2CDE49DD}.Release|Any CPU.Build.0 = Release|Any CPU + {0911EA85-F152-453E-BB7D-4C7079361443}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0911EA85-F152-453E-BB7D-4C7079361443}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0911EA85-F152-453E-BB7D-4C7079361443}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0911EA85-F152-453E-BB7D-4C7079361443}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE