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