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)); } }