using Admin.NET.Plugin.AiDOP.DataPlatform;
using Xunit;
namespace Admin.NET.Plugin.AiDOP.Tests.DataPlatform;
public class EmployeePositionRulesTests
{
[Theory]
[InlineData("计划", "PLANNER")]
[InlineData("仓管", "WAREHOUSE")]
[InlineData("生产", "PRODUCTION")]
[InlineData("成品仓", "WAREHOUSE")]
[InlineData("库管员", "WAREHOUSE")]
[InlineData("物料员", "WAREHOUSE")]
[InlineData("WMS专员", "WAREHOUSE")]
[InlineData("装配工", "PRODUCTION")]
[InlineData("车间班长", "PRODUCTION")]
[InlineData("PMC", "PLANNER")]
[InlineData("调度员", "PLANNER")]
[InlineData("财务", "UNKNOWN")]
[InlineData("", "UNKNOWN")]
[InlineData(null, "UNKNOWN")]
// 165 的 EmployeeMaster.JobTitle 填的是小写英文,中文关键字一个都匹配不上。
[InlineData("warehouse", "WAREHOUSE")]
[InlineData("warehouse1", "WAREHOUSE")]
[InlineData("Warehouse Clerk", "WAREHOUSE")]
[InlineData("wms专员", "WAREHOUSE")]
[InlineData("production", "PRODUCTION")]
[InlineData("Assembly Operator", "PRODUCTION")]
[InlineData("planner", "PLANNER")]
// 已人工确认不属于三类业务岗,归 OTHER 而非 UNKNOWN,不再记 POSITION_UNMAPPED。
[InlineData("quality", "OTHER")]
[InlineData("QC检验员", "OTHER")]
[InlineData("数据员", "OTHER")]
[InlineData("研发工程师", "OTHER")]
// OTHER 判定在三类业务岗之后,含「仓」仍归 WAREHOUSE。
[InlineData("质量仓管", "WAREHOUSE")]
public void Classify_ExactBeforeKeyword(string? raw, string expected)
{
Assert.Equal(expected, EmployeePositionRules.Classify(raw));
}
[Theory]
[InlineData("WAREHOUSE", "WAREHOUSE")]
[InlineData(" PLANNER ", "PLANNER")]
// MySQL 默认排序规则不区分大小写,小写值不挡住就会绕过校验混进库里,故统一归一化为大写标准值。
[InlineData("warehouse", "WAREHOUSE")]
[InlineData("Other", "OTHER")]
[InlineData("WAREHOUSE1", null)]
[InlineData("WAREHOUS", null)]
[InlineData("QUALITY", null)]
[InlineData("", null)]
[InlineData(null, null)]
public void Normalize_OnlyAcceptsAllowedCodes(string? input, string? expected)
{
Assert.Equal(expected, EmployeePositionRules.Normalize(input));
}
/// 岗位码白名单只有这五个:Classify 的全部返回值 + 人工可选值,与 KPI 的等值过滤口径对齐。
[Fact]
public void AllowedCodes_AreExactlyTheFiveNeutralCodes()
{
Assert.Equal(
new[] { "WAREHOUSE", "PRODUCTION", "PLANNER", "OTHER", "UNKNOWN" },
EmployeePositionRules.AllowedCodes);
}
/// 人工保存必须经过白名单,否则写错的岗位码会变成永远匹配不上 KPI 过滤的死值。
[Fact]
public void SavePositions_ValidatesAgainstWhitelist()
{
var service = File.ReadAllText(FindFile(
"server", "Plugins", "Admin.NET.Plugin.AiDOP", "DataPlatform", "MdpSourceMappingService.cs"));
Assert.Contains("EmployeePositionRules.Normalize(row.PositionCode)", service);
Assert.Contains("EmployeePositionRules.AllowedCodes", service);
// 旧写法直接把前端传入值落库,不得复活。
Assert.DoesNotContain("code = row.PositionCode.Trim()", service);
}
/// 前端下拉选项与后端白名单必须一致,否则页面能选出后端拒收的值。
[Fact]
public void FrontendPositionCodeOptions_MatchAllowedCodes()
{
var vue = File.ReadAllText(FindFile(
"Web", "src", "views", "aidop", "data-platform", "sourceMappings.vue"));
var expected = "const positionCodes = ["
+ string.Join(", ", EmployeePositionRules.AllowedCodes.Select(code => $"'{code}'"))
+ "];";
Assert.Contains(expected, vue);
// 岗位码不得退回自由文本输入。
Assert.DoesNotContain("", vue);
}
private static string FindFile(params string[] parts)
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir != null)
{
var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
if (File.Exists(candidate)) return candidate;
dir = dir.Parent;
}
throw new FileNotFoundException(string.Join('/', parts));
}
}