change - 修改仓库任务创建逻辑

master
WenJY 4 weeks ago
parent ba49e8606d
commit 79393ccd4b

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sln.Wcs.Business\Sln.Wcs.Business.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,515 @@
#region << 版 本 注 释 >>
/*--------------------------------------------------------------------
|* (c) 2026 WenJY
|* CLR4.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;
/// <summary>
/// 可测试的 StoreTaskBusiness 子类,用于访问 protected 方法
/// </summary>
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<IBasePathInfoService> _mockBasePathInfoService;
private readonly Mock<ILiveTaskQueueService> _mockLiveTaskQueueService;
private readonly Mock<IBaseStoreInfoService> _mockBaseStoreInfoService;
private readonly TestableStoreTaskBusiness _storeTaskBusiness;
public StoreTaskBusinessTests()
{
_mockBasePathInfoService = new Mock<IBasePathInfoService>();
_mockLiveTaskQueueService = new Mock<ILiveTaskQueueService>();
_mockBaseStoreInfoService = new Mock<IBaseStoreInfoService>();
_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<InvalidOperationException>(() =>
_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<InvalidOperationException>(() =>
_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<InvalidOperationException>(() =>
_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<InvalidOperationException>(() =>
_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<InvalidOperationException>(() =>
_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<string>();
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<BasePathDetails>
{
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<System.Linq.Expressions.Expression<Func<BasePathInfo, bool>>>()))
.Returns(new List<BasePathInfo> { 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<System.Linq.Expressions.Expression<Func<BasePathInfo, bool>>>()))
.Returns(new List<BasePathInfo>());
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<BaseLocationInfo>
{
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<System.Linq.Expressions.Expression<Func<BaseStoreInfo, bool>>>(),
It.IsAny<System.Linq.Expressions.Expression<Func<BaseLocationInfo, bool>>>()))
.Returns(new List<BaseStoreInfo> { 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<System.Linq.Expressions.Expression<Func<BaseStoreInfo, bool>>>(),
It.IsAny<System.Linq.Expressions.Expression<Func<BaseLocationInfo, bool>>>()))
.Returns(new List<BaseStoreInfo>());
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<LiveTaskQueue>()))
.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<LiveTaskQueue>()))
.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
}

@ -23,6 +23,8 @@
#endregion << 版 本 注 释 >> #endregion << 版 本 注 释 >>
using Sln.Wcs.Business.Domain.Enum;
namespace Sln.Wcs.Business.Domain.Dto.FilterLocation; namespace Sln.Wcs.Business.Domain.Dto.FilterLocation;
public class FilterLocationDto public class FilterLocationDto
@ -46,4 +48,14 @@ public class FilterLocationDto
/// 数量 /// 数量
/// </summary> /// </summary>
public string amount {get;set;} public string amount {get;set;}
/// <summary>
/// 任务类型:1-入库;2-出库;
/// </summary>
public TaskTypeEnum taskType { get; set; }
/// <summary>
/// 任务类别1-包材;2-成品;3-托盘
/// </summary>
public TaskCategoryEnum taskCategory { get; set; }
} }

@ -0,0 +1,316 @@
#region << 版 本 注 释 >>
/*--------------------------------------------------------------------
|* (c) 2026 WenJY
|* CLR4.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;
/// <summary>
/// 通用仓储任务业务处理类
/// 支持扩展点:子类可重写虚方法实现特定业务逻辑
/// </summary>
public class StoreTaskBusiness : EntityWrapper
{
protected readonly IBasePathInfoService _basePathInfoService;
protected readonly ILiveTaskQueueService _liveTaskQueueService;
protected readonly IBaseStoreInfoService _baseStoreInfoService;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="basePathInfoService">路径服务</param>
/// <param name="liveTaskQueueService">任务队列服务</param>
/// <param name="baseStoreInfoService">仓库服务</param>
public StoreTaskBusiness(
IBasePathInfoService basePathInfoService,
ILiveTaskQueueService liveTaskQueueService,
IBaseStoreInfoService baseStoreInfoService)
{
_basePathInfoService = basePathInfoService;
_liveTaskQueueService = liveTaskQueueService;
_baseStoreInfoService = baseStoreInfoService;
}
#region 虚方法 - 供子类重写的扩展点
/// <summary>
/// 校验物料(可被子类重写)
/// </summary>
public virtual ValidateMaterialResultDto ValidateMaterial(ValidateMaterialDto validateMaterialDto)
{
throw new NotImplementedException();
}
/// <summary>
/// 创建任务(可被子类重写)
/// </summary>
/// <param name="createTaskDto">创建任务参数包含taskType和taskCategory</param>
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<LiveTaskDetail> 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;
}
/// <summary>
/// 筛选库位(可被子类重写)
/// </summary>
/// <param name="filterLocationDto">筛选库位参数包含taskType和taskCategory</param>
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<Func<BaseStoreInfo, bool>> storeWhere = GetStoreWhere(filterLocationDto.taskType, filterLocationDto.taskCategory);
Expression<Func<BaseLocationInfo, bool>> locationWhere = GetLocationWhere(filterLocationDto, locationStatus);
// 4. 查询仓库和库位
List<BaseStoreInfo> 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;
}
/// <summary>
/// 保存任务(可被子类重写)
/// </summary>
/// <param name="saveTaskDto">保存任务参数</param>
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 受保护虚方法 - 可被子类定制
/// <summary>
/// 校验创建任务参数(可被子类重写添加额外校验)
/// </summary>
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");
}
}
/// <summary>
/// 校验筛选库位参数(可被子类重写添加额外校验)
/// </summary>
protected virtual void ValidateFilterLocationParams(FilterLocationDto filterLocationDto)
{
if (string.IsNullOrEmpty(filterLocationDto.materialCode))
{
throw new InvalidOperationException($"物料编号不允许为 NULL");
}
}
/// <summary>
/// 获取路径信息(可被子类重写定制路径查询逻辑)
/// </summary>
protected virtual BasePathInfo GetPathInfo(CreateTaskDto createTaskDto, TaskTypeEnum taskType, TaskCategoryEnum taskCategory)
{
Expression<Func<BasePathInfo, bool>> 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");
}
/// <summary>
/// 生成任务编号(可被子类重写定制生成规则)
/// </summary>
protected virtual string GenerateTaskCode()
{
return DateTime.Now.ToString("yyyyMMddHHmmss") + new Random().Next(1000, 9999);
}
/// <summary>
/// 获取任务成功的消息(可被子类重写)
/// </summary>
protected virtual string GetTaskSuccessMessage(string taskCode, string pathName, TaskTypeEnum taskType, TaskCategoryEnum taskCategory)
{
return $"{GetCategoryName(taskCategory)}{GetTaskTypeName(taskType)}任务创建成功:{taskCode};关联路径:{pathName}";
}
/// <summary>
/// 获取仓库查询条件(可被子类重写)
/// </summary>
protected virtual Expression<Func<BaseStoreInfo, bool>> GetStoreWhere(TaskTypeEnum taskType, TaskCategoryEnum taskCategory)
{
return x => x.storeType == (int)taskCategory && x.isFlag == 1;
}
/// <summary>
/// 获取库位查询条件(可被子类重写)
/// </summary>
protected virtual Expression<Func<BaseLocationInfo, bool>> GetLocationWhere(FilterLocationDto filterLocationDto, int locationStatus)
{
return x => x.materialCode == filterLocationDto.materialCode &&
x.locationStatus == locationStatus &&
x.isFlag == 1;
}
#endregion
#region 辅助方法
/// <summary>
/// 获取分类名称
/// </summary>
protected string GetCategoryName(TaskCategoryEnum taskCategory)
{
return taskCategory switch
{
TaskCategoryEnum.Material => "包材",
TaskCategoryEnum.Product => "成品",
TaskCategoryEnum.Pallet => "托盘",
_ => "未知"
};
}
/// <summary>
/// 获取任务类型名称
/// </summary>
protected string GetTaskTypeName(TaskTypeEnum taskType)
{
return taskType switch
{
TaskTypeEnum.InStore => "入库",
TaskTypeEnum.OutStore => "出库",
_ => "未知"
};
}
#endregion
}

@ -0,0 +1,156 @@
#region << 版 本 注 释 >>
/*--------------------------------------------------------------------
|* (c) 2026 WenJY
|* CLR4.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;
/// <summary>
/// 通用仓储任务业务扩展基类
/// 当某个业务场景需要定制特殊逻辑时,继承此类
/// </summary>
/// <typeparam name="TValidateMaterialDto">物料校验DTO类型</typeparam>
public abstract class StoreTaskBusinessEx<TValidateMaterialDto> : StoreTaskBusiness
where TValidateMaterialDto : ValidateMaterialDto
{
protected StoreTaskBusinessEx(
IBasePathInfoService basePathInfoService,
ILiveTaskQueueService liveTaskQueueService,
IBaseStoreInfoService baseStoreInfoService)
: base(basePathInfoService, liveTaskQueueService, baseStoreInfoService)
{
}
/// <summary>
/// 重写物料校验(示例)
/// </summary>
public override ValidateMaterialResultDto ValidateMaterial(ValidateMaterialDto validateMaterialDto)
{
// 调用子类自定义的校验逻辑
return ValidateMaterialCore((TValidateMaterialDto)validateMaterialDto);
}
/// <summary>
/// 子类实现具体的物料校验逻辑
/// </summary>
protected abstract ValidateMaterialResultDto ValidateMaterialCore(TValidateMaterialDto validateMaterialDto);
}
/// <summary>
/// =============================================================
/// 以下为具体业务扩展示例(使用时取消注释并修改)
/// =============================================================
// ===== 示例1包材入库特殊业务 =====
//public class MaterialInStoreEx : StoreTaskBusiness
//{
// public MaterialInStoreEx(
// IBasePathInfoService basePathInfoService,
// ILiveTaskQueueService liveTaskQueueService,
// IBaseStoreInfoService baseStoreInfoService)
// : base(basePathInfoService, liveTaskQueueService, baseStoreInfoService)
// {
// }
//
// /// <summary>
// /// 重写任务编号生成规则(包材特殊规则)
// /// </summary>
// protected override string GenerateTaskCode()
// {
// // 示例:包材入库任务编号以 "M" 开头
// return "M" + DateTime.Now.ToString("yyyyMMddHHmmss") + new Random().Next(100, 999);
// }
//
// /// <summary>
// /// 重写入库成功的消息
// /// </summary>
// 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)
// {
// }
//
// /// <summary>
// /// 重写库位查询条件(成品出库需要特殊校验)
// /// </summary>
// protected override Expression<Func<BaseLocationInfo, bool>> GetLocationWhere(FilterLocationDto filterLocationDto, int locationStatus)
// {
// // 成品出库可能有额外的有效期校验等
// return x => x.materialCode == filterLocationDto.materialCode &&
// x.locationStatus == locationStatus &&
// x.isFlag == 1 &&
// x.remark != "冻结"; // 排除冻结库位
// }
//
// /// <summary>
// /// 重写任务编号生成规则(成品特殊规则)
// /// </summary>
// 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)
// {
// }
//
// /// <summary>
// /// 重写创建任务参数校验(托盘可能有重量校验)
// /// </summary>
// protected override void ValidateCreateTaskParams(CreateTaskDto createTaskDto)
// {
// base.ValidateCreateTaskParams(createTaskDto);
//
// // 添加托盘特有的校验逻辑
// // if (createTaskDto.Weight > 1000) throw new InvalidOperationException("托盘重量超限");
// }
//}

@ -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 # Visual Studio Version 17
VisualStudioVersion = 17.10.35122.118 VisualStudioVersion = 17.10.35122.118
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
@ -28,6 +28,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.Business", "Sln.Wcs
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.Strategy", "Sln.Wcs.Strategy\Sln.Wcs.Strategy.csproj", "{F7658F97-F78A-4612-A1A5-490F2CDE49DD}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.Strategy", "Sln.Wcs.Strategy\Sln.Wcs.Strategy.csproj", "{F7658F97-F78A-4612-A1A5-490F2CDE49DD}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{F7658F97-F78A-4612-A1A5-490F2CDE49DD}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

Loading…
Cancel
Save