소스 검색

fix(s5/s7): 岗位关键词表补英文,补齐人效指标在职分母 | server 1.0.585

根因:EmployeePositionRules 的关键词表只有中文(仅 WMS 一个英文缩写),
而 165 的 EmployeeMaster.JobTitle 填的是小写英文 warehouse/warehouse1/quality,
一个都匹配不上,26 名员工的 position_code 全落 UNKNOWN,
导致 S5_L1_003 / S7_L1_003 人效分母为 0。T8 的 gw 是中文故不受影响。

- WarehouseKeywords/ProductionKeywords/PlannerKeywords 各补英文词
- 新增 OtherKeywords(quality/质量/QC/QA/数据员/研发),判定置于三类业务岗之后,
  含「仓」的质量岗仍归 WAREHOUSE;OTHER 与 UNKNOWN 均不进人效分母,
  区别只在 UNKNOWN 会记 POSITION_UNMAPPED 并进待映射清单
- 关键词比对统一 OrdinalIgnoreCase:岗位原文大小写不由平台控制
- Classify 兜底仍为 UNKNOWN,未见过的新岗位继续走 POSITION_UNMAPPED 告警
- 补 12 个用例覆盖英文岗位、大小写不敏感、OTHER 判定次序

不需要数据脚本:SyncNativeAsync 挂在 NativeNeutralProjectionService 内,
且在 mdp_std_employee 投影之前执行,部署后第一次模块重算自动重分类;
role_source='MANUAL' 的人工行仍受 UPSERT 保护不被覆盖。

测试:3179 个用例,7 个失败均为改动前既有基线,零回归。

Co-authored-by: Cursor <cursoragent@cursor.com>
YY968XX 2 일 전
부모
커밋
a5261a4716

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.584</AssemblyVersion>
-    <FileVersion>1.0.584</FileVersion>
-    <Version>1.0.584</Version>
+    <AssemblyVersion>1.0.585</AssemblyVersion>
+    <FileVersion>1.0.585</FileVersion>
+    <Version>1.0.585</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 15 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/DataPlatform/EmployeePositionRulesTests.cs

@@ -20,6 +20,21 @@ public class EmployeePositionRulesTests
     [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));

+ 19 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/EmployeePositionRules.cs

@@ -1,7 +1,9 @@
 namespace Admin.NET.Plugin.AiDOP.DataPlatform;
 
 /// <summary>
-/// 岗位原文 → 中立岗位码。精确值优先于关键字。未命中为 UNKNOWN,不计入在职分母。
+/// 岗位原文 → 中立岗位码。精确值优先于关键字。
+/// S3/S5/S6/S7 人效分母按 PLANNER/WAREHOUSE/PRODUCTION 逐个过滤,故 OTHER 与 UNKNOWN 均不进分母;
+/// 两者的差别只在运维可见性:UNKNOWN 会记 POSITION_UNMAPPED 并进待映射清单,OTHER 不会。
 /// </summary>
 public static class EmployeePositionRules
 {
@@ -11,9 +13,12 @@ public static class EmployeePositionRules
     public const string Other = "OTHER";
     public const string Unknown = "UNKNOWN";
 
-    public static readonly string[] WarehouseKeywords = ["仓", "库管", "保管", "物料员", "WMS"];
-    public static readonly string[] ProductionKeywords = ["生产", "操作", "装配", "车间", "班组", "产线", "工段"];
-    public static readonly string[] PlannerKeywords = ["计划", "PMC", "调度"];
+    public static readonly string[] WarehouseKeywords = ["仓", "库管", "保管", "物料员", "WMS", "warehouse", "storekeeper"];
+    public static readonly string[] ProductionKeywords = ["生产", "操作", "装配", "车间", "班组", "产线", "工段", "production", "assembly", "operator", "workshop"];
+    public static readonly string[] PlannerKeywords = ["计划", "PMC", "调度", "planner", "planning", "scheduler"];
+
+    /// <summary>已确认不属于仓管/生产/计划的岗位,归 OTHER 后不再记 POSITION_UNMAPPED 告警。</summary>
+    public static readonly string[] OtherKeywords = ["quality", "质量", "QC", "QA", "数据员", "研发"];
 
     /// <summary>T8 gw 原值精确匹配,优先于关键字。</summary>
     public static readonly IReadOnlyDictionary<string, string> Exact = new Dictionary<string, string>(StringComparer.Ordinal)
@@ -30,12 +35,19 @@ public static class EmployeePositionRules
         var text = raw.Trim();
         if (Exact.TryGetValue(text, out var exact))
             return exact;
-        if (WarehouseKeywords.Any(text.Contains))
+        if (Matches(text, WarehouseKeywords))
             return Warehouse;
-        if (ProductionKeywords.Any(text.Contains))
+        if (Matches(text, ProductionKeywords))
             return Production;
-        if (PlannerKeywords.Any(text.Contains))
+        if (Matches(text, PlannerKeywords))
             return Planner;
+        // OTHER 放在三类业务岗之后:岗位原文同时含「质量」与「仓」时仍归 WAREHOUSE。
+        if (Matches(text, OtherKeywords))
+            return Other;
         return Unknown;
     }
+
+    // 岗位原文的大小写不由平台控制(165 的 JobTitle 是小写英文,T8 的 gw 是中文),统一忽略大小写比对。
+    private static bool Matches(string text, string[] keywords)
+        => keywords.Any(keyword => text.Contains(keyword, StringComparison.OrdinalIgnoreCase));
 }