EmployeePositionRulesTests.cs 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. using Admin.NET.Plugin.AiDOP.DataPlatform;
  2. using Xunit;
  3. namespace Admin.NET.Plugin.AiDOP.Tests.DataPlatform;
  4. public class EmployeePositionRulesTests
  5. {
  6. [Theory]
  7. [InlineData("计划", "PLANNER")]
  8. [InlineData("仓管", "WAREHOUSE")]
  9. [InlineData("生产", "PRODUCTION")]
  10. [InlineData("成品仓", "WAREHOUSE")]
  11. [InlineData("库管员", "WAREHOUSE")]
  12. [InlineData("物料员", "WAREHOUSE")]
  13. [InlineData("WMS专员", "WAREHOUSE")]
  14. [InlineData("装配工", "PRODUCTION")]
  15. [InlineData("车间班长", "PRODUCTION")]
  16. [InlineData("PMC", "PLANNER")]
  17. [InlineData("调度员", "PLANNER")]
  18. [InlineData("财务", "UNKNOWN")]
  19. [InlineData("", "UNKNOWN")]
  20. [InlineData(null, "UNKNOWN")]
  21. // 165 的 EmployeeMaster.JobTitle 填的是小写英文,中文关键字一个都匹配不上。
  22. [InlineData("warehouse", "WAREHOUSE")]
  23. [InlineData("warehouse1", "WAREHOUSE")]
  24. [InlineData("Warehouse Clerk", "WAREHOUSE")]
  25. [InlineData("wms专员", "WAREHOUSE")]
  26. [InlineData("production", "PRODUCTION")]
  27. [InlineData("Assembly Operator", "PRODUCTION")]
  28. [InlineData("planner", "PLANNER")]
  29. // 已人工确认不属于三类业务岗,归 OTHER 而非 UNKNOWN,不再记 POSITION_UNMAPPED。
  30. [InlineData("quality", "OTHER")]
  31. [InlineData("QC检验员", "OTHER")]
  32. [InlineData("数据员", "OTHER")]
  33. [InlineData("研发工程师", "OTHER")]
  34. // OTHER 判定在三类业务岗之后,含「仓」仍归 WAREHOUSE。
  35. [InlineData("质量仓管", "WAREHOUSE")]
  36. public void Classify_ExactBeforeKeyword(string? raw, string expected)
  37. {
  38. Assert.Equal(expected, EmployeePositionRules.Classify(raw));
  39. }
  40. [Theory]
  41. [InlineData("WAREHOUSE", "WAREHOUSE")]
  42. [InlineData(" PLANNER ", "PLANNER")]
  43. // MySQL 默认排序规则不区分大小写,小写值不挡住就会绕过校验混进库里,故统一归一化为大写标准值。
  44. [InlineData("warehouse", "WAREHOUSE")]
  45. [InlineData("Other", "OTHER")]
  46. [InlineData("WAREHOUSE1", null)]
  47. [InlineData("WAREHOUS", null)]
  48. [InlineData("QUALITY", null)]
  49. [InlineData("", null)]
  50. [InlineData(null, null)]
  51. public void Normalize_OnlyAcceptsAllowedCodes(string? input, string? expected)
  52. {
  53. Assert.Equal(expected, EmployeePositionRules.Normalize(input));
  54. }
  55. /// <summary>岗位码白名单只有这五个:Classify 的全部返回值 + 人工可选值,与 KPI 的等值过滤口径对齐。</summary>
  56. [Fact]
  57. public void AllowedCodes_AreExactlyTheFiveNeutralCodes()
  58. {
  59. Assert.Equal(
  60. new[] { "WAREHOUSE", "PRODUCTION", "PLANNER", "OTHER", "UNKNOWN" },
  61. EmployeePositionRules.AllowedCodes);
  62. }
  63. /// <summary>人工保存必须经过白名单,否则写错的岗位码会变成永远匹配不上 KPI 过滤的死值。</summary>
  64. [Fact]
  65. public void SavePositions_ValidatesAgainstWhitelist()
  66. {
  67. var service = File.ReadAllText(FindFile(
  68. "server", "Plugins", "Admin.NET.Plugin.AiDOP", "DataPlatform", "MdpSourceMappingService.cs"));
  69. Assert.Contains("EmployeePositionRules.Normalize(row.PositionCode)", service);
  70. Assert.Contains("EmployeePositionRules.AllowedCodes", service);
  71. // 旧写法直接把前端传入值落库,不得复活。
  72. Assert.DoesNotContain("code = row.PositionCode.Trim()", service);
  73. }
  74. /// <summary>前端下拉选项与后端白名单必须一致,否则页面能选出后端拒收的值。</summary>
  75. [Fact]
  76. public void FrontendPositionCodeOptions_MatchAllowedCodes()
  77. {
  78. var vue = File.ReadAllText(FindFile(
  79. "Web", "src", "views", "aidop", "data-platform", "sourceMappings.vue"));
  80. var expected = "const positionCodes = ["
  81. + string.Join(", ", EmployeePositionRules.AllowedCodes.Select(code => $"'{code}'"))
  82. + "];";
  83. Assert.Contains(expected, vue);
  84. // 岗位码不得退回自由文本输入。
  85. Assert.DoesNotContain("<el-input v-model=\"row.positionCode\" />", vue);
  86. }
  87. private static string FindFile(params string[] parts)
  88. {
  89. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  90. while (dir != null)
  91. {
  92. var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
  93. if (File.Exists(candidate)) return candidate;
  94. dir = dir.Parent;
  95. }
  96. throw new FileNotFoundException(string.Join('/', parts));
  97. }
  98. }