Просмотр исходного кода

优化工序限制,添加人员信息显示

callm 1 день назад
Родитель
Сommit
78c531c1a0

+ 11 - 0
UAS_MES_YDCY/.workbuddy/memory/2026-10-27.md

@@ -0,0 +1,11 @@
+# 2026-10-27
+
+## Query_LoadMan:上线时记录人员名称(有则记、无则不卡)
+- 用户要求:上线时若工号存在于 `SOPEMPLOYEEDETAIL` 就顺带记录姓名(`lm_emname`),不存在只记工号,**不做存在性卡控**。
+- 实现(只改 `Query_LoadMan.cs` + `Query_LoadMan.Designer.cs`):
+  - 上线分支:`getFieldDataByCondition("SOPEMPLOYEEDETAIL","max(sed_emname)","sed_emcode='...'")` 查姓名;
+    空则 insert 不带 `lm_emname`,非空则带列写入;成功提示带上姓名(有才带)。
+  - DGV 重新加回「人员名称」列(`lm_emname`,DataPropertyName=lm_emname,Width=120,放 lm_emcode 后),
+    `LoadGridData` select 加 `lm.lm_emname`(不 join SOPEMPLOYEEDETAIL,库里没有就显示空)。
+- 保留了:只扫工号不解析格式、批量勾选下线、全选按钮、自动互斥下线、回车 KeyPress 拦截。
+- 注意:历史记录(之前只记工号)该列为空属预期;`lm_emname` 列需可空(本次空值时不写该列)。

+ 35 - 222
UAS_MES_YDCY/.workbuddy/memory/MEMORY.md

@@ -1,228 +1,41 @@
 # UAS_MES_YDCY 项目约定
 
-## 技术栈
-- WinForms,.NET Framework,老式 csproj + packages.config;数据库为 Oracle。
-- 主程序集输出名 `UAS_MES_NEW.exe`(在 `bin/Debug/`)。
-
-## 启动入口(Program.cs)
-- `Main` 用 `WindowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator)` 判断:是管理员 → `Application.Run(new Login())`;否则用 `StartInfo.Verb="runas"` 重启自己走 UAC。
-- **实例数限制**:`MaxInstanceCount = 3` + `CheckInstanceLimit()`(按 `Process.GetProcessesByName` 计数,含自身),
-  检测**必须放在管理员分支内、Application.Run 之前**——否则 UAC 提权会让一次启动被数两次而误拦。
-
-## 新增一个功能界面(标准步骤)
-1. 在 `FunctionCode/<模块>/` 下建 3 个文件:`Xxx.cs`、`Xxx.Designer.cs`、`Xxx.resx`。
-   - resx 直接 `cp` 一个现有空模板(如 `Make_SplitBoard.resx`,5817 字节)即可。
-2. `namespace UAS_MES_NEW.<模块>`,class 继承 `Form`。
-3. Designer 风格:`FormBorderStyle = None`、`Tag = "<模块>!<类名>"`、`AutoScaleDimensions(8F,15F)` + `AutoScaleMode.Font`。
-4. csproj 三处注册(缺一不可):
-   - `<Compile Include="...\Xxx.cs"><SubType>Form</SubType></Compile>`
-   - `<Compile Include="...\Xxx.Designer.cs"><DependentUpon>Xxx.cs</DependentUpon></Compile>`
-   - `<EmbeddedResource Include="...\Xxx.resx"><DependentUpon>Xxx.cs</DependentUpon></EmbeddedResource>`
-5. 菜单:无需改代码。左侧导航由数据库驱动——`CS$SYSNAVATION`(`sn_classname` 填 `模块.类名`,`sn_displayname` 为菜单名,`sn_module` 为分组名,`sn_caller` 为权限 Caller,`sn_using=1`),权限在 `CS$USERGROUPPOWER`。运行时按 `Assembly.GetExecutingAssembly().CreateInstance("UAS_MES_NEW." + sn_classname)` 反射开窗。
-   - **实际生效的是 `NavagationBar`**(`Main.Designer.cs` 里 new 的是它,不是同目录的 `AccordionMenu`)。分组由 `sn_module` 动态创建,所以 `sn_module` 可以是 `3-SPECIAL` 这种名字;`AccordionMenu` 则要求 `sn_module` 匹配 Designer 里硬编码的 ListView 名(如 `特殊作业`)——改错类会白干。
-   - 分组图标取 `Resources/<sn_module>.png`,取不到就回退 `defaulticon.png`(如 `3-SPECIAL.png` 不存在,会走回退,功能正常)。
-   - ⚠️ `NavagationBar` 打开窗体时执行 `OpenForm.Tag = sn_caller`,**运行时会覆盖 Designer 里的 Tag** → 代码中 `DoCommandLog(Tag.ToString(), ...)` 实际记的是 `sn_caller`,命名要考虑审计日志可读性。
-   - 非 admin 账号的菜单查询带 `nvl(ugp_ifread,0)<>0 or nvl(ugp_ifall,0)<>0` 过滤 → 新 Caller 必须补 `CS$USERGROUPPOWER` 记录,否则只 admin 可见。
-
-## 复制一个已有窗体(改名派生)
-照抄三件套后必须同步改 5 处,漏一处就编不过或取不到图片资源:
-1. `.cs`:`class` 名、构造函数名、`Load` 事件方法名(`Xxx_Load`)
-2. `.Designer.cs`:`partial class` 名、**`ComponentResourceManager(typeof(Xxx))`(最易漏)**、`this.Name`、`this.Tag`、`this.Text`、`this.Load +=` 的方法名
-3. `.resx`:**内容无需改动**,字节复制即可——资源归属由 `typeof()` 决定,资源键名(`Confirm.DownImage` 等)不含窗体名
-4. csproj 三处注册
-> 注:本机 Git Bash coreutils 不可用(`ls`/`cp`/`head` 都 command not found),复制二进制文件用 Python `shutil.copyfile`。
-
-## 界面通用写法(从现有代码提炼)
-- 字段:`AutoSizeFormClass asc = new AutoSizeFormClass();`、`DataHelper dh;`、`DataTable dt;`
-- `Load` 事件:`asc.controllInitializeSize(this);` → `dh = SystemInf.dh;` → 初始化数据 → `xxx.Focus();`
-- `SizeChanged` 事件:`asc.controlAutoSize(this);`
-- 日志面板:`OperateResult.AppendText(">>提示\n", Color.Black)` / `("<<错误\n", Color.Red, 控件)`(`RichTextAutoBottom`,含 (string,Color) 与 (string,Color,Control) 两个重载)
-
-## 常用自定义控件(`UAS_MES_NEW.CustomControl.*`)
-- SN/条码输入:`TextBoxWithIcon.SnCollectionBox`(继承 `EnterTextBox`,额外属性 `AllPower/ID/Power/Str/Str1/Str2`)
-- 任意表查询放大镜:`TextBoxWithIcon.SearchTextBox`(属性 `TableName/SelectField(字段 # 中文)/SetValueField/FormName/Condition/DBTitle/TextBoxEnable`;`DbChange` 事件取 `ReturnData` 配 `BaseUtil.SetFormValue` 回填;`TextBoxEnable=false` 时只能放大镜选择)
-- 工单输入(带放大镜 DbFind):`TextBoxWithIcon.MaCodeSearchTextBox`,配 `TableName/SelectField/SetValueField/FormName/Condition`
-- 按钮:`ButtonUtil.NormalButton`(未设 NormalImage/DownImage/MoveImage 时自动取 `Properties.Resources` 默认图)
-- 结果面板:`RichText.RichTextAutoBottom`
-- 打印机下拉:`ComBoxWithFocus.PrinterCombox`(`Load` 时自动填 `PrinterSettings.InstalledPrinters` 并选默认打印机,`.Text` 即打印机名)
-- 打印模板下拉:`ComBoxWithFocus.ComboxWithTip`(`ComboBox`,配 `DataSource/DisplayMember=la_url/ValueMember=la_id`)
-- 自适应:`PublicMethod.AutoSizeFormClass`(internal,同程序集内可用)
-
-## 打印流程范式(BarTender)
-- `ma_prodcode_TextChanged` 内按产品编号查模板:`select la_id,la_url,la_isdefault from label where la_prodcode='xx' and la_templatetype='机身标' and la_statuscode='AUDITED' order by la_isdefault`,再 `DataSource = _dt.Copy()` 分给每个模板下拉(多个下拉必须各自 `Copy()`,共用同一 DataTable 会互相干扰)。
-- 份数带出:`dh.getFieldDataByCondition("label","la_printnos","la_id='xx'")`,空则默认 `"1"`;每个模板下拉的 `SelectedIndexChanged` 各绑一次。
-- 调用:`Print.BarTender(Tag.ToString(), ref engine, 模板URL, la_id, 打印机名, SN, 份数, 工单号, 产品编号, "机身标", "0", out ErrorMessage)`,返回 bool。
-- 同类可抄界面:`Make_ColorBoxLabelPrintMidBox`(含「打印副标签」勾选 + 第二组打印机/模板/份数,布局最规整)。
-
-## Designer 布局经验(多组控件)
-- 组间垂直间距按**最长组**(勾选框 + 3 行输入)预留约 **180px**,否则一定撞到下方面板。
-- 同一行内 Label 与输入框垂直居中:Label 设 `AutoSize=false` + 高度匹配输入框 + `TextAlign=MiddleLeft`,比反复调 `Location.Top` 稳。
-- `ComboBox`(DropDownList)实际高度由字体决定,`Size.Height` 被忽略;放高处的下拉不要靠 Height 对齐。
-- `TextBox` 单行高度锁死,`Dock` 会覆盖 `Top`。
-- 大日志面板 `RichTextAutoBottom` 字体设为 12F 后,`AutoSizeFormClass` 放大窗体时它会跟着长高 → 需留够余量,否则会压住右下角控件。
-
-## 备注输入域范式(commandlog 附加备注)
-特殊界面(Special_*)加备注的固定写法,参考 `Special_Reset`:
-- Designer:`Remark_label`(`Label`,微软雅黑 12F,`AutoSize=true`,Size(82,41))+ `Remark`(**`CustomControl.RichText.RichTextAutoBottom`,多行,不是 EnterTextBox**)。Reset 系列坐标 `(947,290)` / `(1038,290)` Size(448,241),位于按钮行(y216~260)正下方。
-- `Controls.Add` 要放最前面;字段声明放最后。
-- .cs 内加:
-  ```csharp
-  private string ResultWithRemark(string result)
-  {
-      string remark = Remark.Text.Trim();
-      return remark == "" ? result : result + "," + remark;
-  }
-  ```
-  再把每个 `DoCommandLog` 的 result 参数包成 `ResultWithRemark(...)`。注意原有 result 里已含逗号时(如 `"Key"+xx+"解冻,站点重置至"+stname`)拼接后会出现两个逗号,属预期。
-- 已套用:`Special_Reset`、`Special_ResetKeyFreeze`、`Special_ResetKeyRelease`、`Special_CancelCollection`(后者布局不同,备注放 `(68,362)`/`(226,358)` 554×280,正文 10.5F)。
-
-## DGV 勾选批量操作范式
-参考 `Query_LoadMan`(人员批量下线)、`Query_LoadMake`(批量切换人数):
-- **Designer**:`DataGridViewCheckBoxColumn`(`HeaderText="勾选"`,`Width≈70`,**不设 `DataPropertyName`** 避免绑定报错),
-  放进 `AddRange` 数组最前 → DGV 最左列;配套 `ButtonUtil.NormalButton`(如 `Text="确认下线"`),
-  `Controls.Add(按钮)` 放列表**最前**(z-order 最底,不会被遮挡)。
-- **判断是否有勾选**:用 `Cells["列名"].FormattedValue.ToString() == "True"`,
-  **不要用 `.Value`**——未手动点过的复选框 `.Value` 可能是 `null`,而 `FormattedValue` 已是 `"False"`。
-- **批量更新**:遍历 `DGV.Rows` 筛勾选行 → 按**主键**逐条 update(`Query_LoadMan` 用 `lm_id`),
-  逐条 try/catch 累计成功/失败数,最后汇总一行提示 + 刷新方法。
-- 提示输出用 **(string, Color) 两参数重载**,避开 (string,Color,Control) 重载内部 `ctl.Text=""` 误清输入框。
-- 刷新后勾选状态会因重新绑定被清空,属预期。
-- **全选按钮**(切换式):先扫一遍判断是否已全部勾选,`bool setValue = !allChecked` → 点一下全勾、再点一下全不勾;
-  逐行 `Cells["列名"].Value = setValue`,再调 **`DGV.RefreshEdit()`** 让复选框立即重绘
-  (只改 cell value 不调 `RefreshEdit()` 时显示可能不刷新)。**不要顺便调刷新方法**——全选只改界面状态,不该重新查库。
-- 按钮文案跟随状态切换(`"全选"` ⇄ `"取消全选"`)时,**必须同时在数据刷新方法末尾复位文案**
-  (重新绑定后复选框必然清空),否则会出现「按钮写着取消全选、实际一个都没勾」的错位。
-- ⚠️ 布局坑:`RadioButton` 等 `AutoSize=true` 的控件,`Size` 属性写了也不生效,实际宽度由文本决定。
-  给新按钮找位置时**不能只看 `Location`**,要按「Location.X + 实际文本宽度」估算右边界,否则会撞上。
-- ⚠️ **删除 DGV 列要同时改 3 处**:`AddRange` 数组、该列的属性设置块(`// 列名` 注释段)、
-  末尾的 `private ... 列字段;` 声明。漏任一处要么编译报错(字段不存在),要么运行期多/少一列。
-  若 `.cs` 里有 `row.Cells["列名"]` 引用该列,**必须一起删**,否则运行期抛 `ArgumentException`。
-- **手工累加行(不用 DataSource)**:`int idx = dgv.Rows.Add(); dgv.Rows[idx].Cells["列Name"].Value = ...`
-  —— 用列的 **`Name`** 取值(不是 `HeaderText`)。
-  ⚠️ **累加前必须先解 DataSource**:`dgv.DataSource` 非 null 时调 `Rows.Add()` 会抛
-  `InvalidOperationException`。同一界面的另一模式若用过 `FillDgvWithDataTable`(会设 DataSource),
-  模式切换后就会踩到。加 `if (dgv.DataSource != null) { dgv.DataSource = null; dgv.Rows.Clear(); }`。
-- **`ChooseAllButton`(`CustomControl.ButtonUtil`)的状态复位问题**:
-  `ChooseAll(dgv)` 内部 `Click +=` 一个用**私有 `AllChecked` 字段**记状态的处理器,**外部无法复位**;
-  一旦界面会「清空列表 / 累加新行」,按钮语义就会与实际勾选错位。
-  → 因公共控件不许改,**该界面改为不调 `ChooseAll(dgv)`,自己 `Click +=` 本地处理器 + 本地 `_allChecked`**,
-  并抽出 `ResetChooseAllState()` 在所有清空点调用。其他界面仍走原 `ChooseAll(dgv)`,互不影响。
-
-## 扫码输入框(SnCollectionBox)的回车坑 —— 只在功能界面内解决
-- `SnCollectionBox_KeyDown`(在 `SnCollectionBox.Designer.cs` 里 hook)的 Enter 分支末尾有 **`this.SelectAll()`**。
-  因 Windows 消息顺序是 `KeyDown` → 字符输入,**在 `KeyDown` 里 SelectAll 会把刚扫进来的整串内容全选中,下一个字符直接覆盖** → 界面只收到最后几个字符。
-  → **界面自己处理回车时,绝对不要在 `KeyDown` 回调里调 `SelectAll()`。**
-- ⚠️ **约束:公共控件(`CustomControl/` 下)不许改,只能在功能界面内解决。** 用户明确要求过。
-- ✅ **推荐做法(零侵入)**:在功能界面对该控件**额外 hook `KeyPress`**,用字符级拦截吞掉 `\r`:
-  ```csharp
-  // Designer: this.em_code.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.Em_code_KeyPress);
-  private void Em_code_KeyPress(object sender, KeyPressEventArgs e)
-  {
-      if (e.KeyChar == '\r' || e.KeyChar == '\n')
-      {
-          e.Handled = true;      // 吞掉字符级回车
-          HandleEmCode();        // 自己的处理
-      }
-  }
-  ```
-  **原理**:`SnCollectionBox` / `EnterTextBox` 的回车处理**全部挂在 `KeyDown` 上,二者都不注册 `KeyPress`**;
-  而 `KeyPress` 在 `KeyDown` 之后、字符写入控件之前触发,`Handled = true` 能阻止该字符进入控件
-  → 控件的 `KeyDown` 就收不到回车,`SelectAll()` 自然不执行。
-- **建议同时 hook `KeyDown` 做双保险**(挡住以 Tab 结尾的扫描枪),并加防重入开关:
-  ```csharp
-  // KeyDown 里: e.SuppressKeyPress = true; e.Handled = true; HandleEmCode();
-  private bool _emBusy = false;
-  private void HandleEmCode() { if (_emBusy) return; _emBusy = true; try { DoEmCode(); } finally { _emBusy = false; } }
-  ```
-- **事件签名只能用 `(object, KeyEventArgs)` 或 `(object, KeyPressEventArgs)`**:
-  用 `KeyPressEventHandler` 却写 `KeyEventArgs` 参数会静默不触发。
-- ⚠️ 两个 args 类成员同名不同用途:**`SuppressKeyPress` 只在 `KeyEventArgs` 上**,`KeyPressEventArgs` 只有 `Handled`。
-  `KeyEventArgs.Handled = true` **不会**阻止本控件其他订阅者,只是标记「已处理」。
-- 不要让控件自己 hook `TextChanged` 去解析——**在 Form 的 `KeyPress`/`KeyDown` 里处理回车最可靠**,因为控件内部顺序不可控。
-- 查事件注册源头时**必须派生类 + 基类一起看**:`SnCollectionBox` 与其基类 `EnterTextBox` 都只在 `KeyDown` 上做文章;
-  而 `RichTextAutoBottom` 的 `InitializeComponent` 只做 `components = new Container()`,**不注册任何事件**,
-  所以 `AppendText(str, Color, ctl)` 的 `ctl.Text=""` 不会重复触发扫描框回车。
-
-## 扫描格式解析:工号/人员姓名(⚠️ 方案最终被用户放弃,见下)
-- ~~扫描枪常输出 `工号/人员姓名` 两段~~ —— **`Query_LoadMan` 已改回「只扫工号、只记工号」**,
-  该解析整体删除(含 `SplitEmInfo` 方法)。**不要再为新界面默认加姓名解析。**
-- 教训:**不要凭「扫描枪常见格式」自行加姓名解析和人员存在性卡控**。
-  `SOPEMPLOYEEDETAIL` 未必有全部在岗人员,卡控会让人上不了线;
-  名字属于附加信息,若用户没明确要求「显示姓名/校验人员」,就只用工号。
-- 保留此节仅因分隔符集合仍可能在别处有用:`{ "/", "\\", "/", "|", "|", " ", " ", "、" }`;
-  用 `text.Split(sep, StringSplitOptions.RemoveEmptyEntries)`(⚠️ 此重载**不带 `TrimEntries`**,.NET Core 3.0+ 才有),
-  只能手动 `.Trim()`;**`\r` 不要放进分隔符集合**,靠末段 `.Trim()` 清掉即可。
-
-
-## 工艺路线三表关联(makeserial + craft + craftdetail)
-项目里反复出现同一组关联,记住键名可以少查:
-- `craft`:`cr_id`(主键)/ `cr_code`(工艺路线编号)/ `cr_prodcode`(产品编号)
-- `craftdetail`:`cd_crid` → `craft.cr_id` / `cd_stepcode`(工序编号)/ `cd_stepno` / `cd_detno` / `cd_steptype`(工序类型)
-- `makeserial`:`ms_craftcode`(工艺路线编号)/ `ms_prodcode`(产品编号)/ `ms_stepcode`(当前工序)
-- **两种起头方式**(都见过,按场景选):
-  - 从 makeserial 起头(已知序列号):`craft on cr_code=ms_craftcode and cr_prodcode=ms_prodcode` +
-    `craftdetail on cd_crid=cr_id and cd_stepcode=ms_stepcode`
-  - 从 craftdetail 起头(已知产品+工艺):`craftdetail left join craft on cr_id=cd_crid`,条件里再限定 `cd_stepcode`
-- ⚠️ **追加 join 时务必用 `left join`**:原查询可能只 join 了 product,多 join 两张表若是 inner join,
-  工艺明细缺一条就会**静默少行**导致 `dt.Rows.Count == 0`,表现为「序列号查不到」。
-- `steptype`(`STP_CODE`/`STP_NAME`)是工序类型的字典表,`cd_steptype` 存的是它的编码。
-- `productcheckrate` + `PRODUCTCHECKRATEDETAIL`:`pcd_pcrid=pcr_id`,主表 `pcr_prodcode`,
-  明细侧有 `pcd_stepcode` / `pcd_steptype` / `pcd_qualmethod` / `pcd_custqualmethod` / `pcd_okretuenstep` / `pcd_ngretuenstep`。
-- **查配置类表时,优先用「更具体的条件查一次、查不到再放宽条件重查」的两段式,而不是用 `or` 把条件并列**:
-  后者在有多个候选行时结果不确定(Oracle 不保证返回哪一行),前者优先级明确、也便于加日志。
-  模板见 `OQC_PlanMaintain_Cust.GetProductCheckRate()` / `OQC_PlanMaintain_NEW.GetProductCheckRate()`。
-- ⚠️ **拼「按某字段过滤」的条件前,必须先判该字段非空**:若该字段为空串(如 `cd_steptype` 取不到),
-  会拼出 `and pcd_steptype=''`,要么错误命中脏数据行、要么返回 0 行让回退逻辑误触发。
-  正确做法是空值直接跳过这一段查询,语义上等价于「不加这个条件」。
-- 现成参照:`Make_ColorBoxLabelPrint.cs`、`Make_ColorBoxLabelPrintMidBox.cs`、`Make_ColorBoxLoadPrintPT.cs`
-  都用 `getFieldDataByCondition("craftdetail left join craft on cr_id=cd_crid left join makeserial on ms_craftcode=cr_code and cr_prodcode=ms_prodcode", ...)` 取工艺明细上的标志位。
-
-## 把逻辑从一个界面「覆盖」到它的兄弟界面(同族窗体移植)
-项目里常见 `Xxx_Cust` / `Xxx_NEW` 这种同族窗体(还有 `Xxx` 本体),逻辑要同步但**不能整段照抄**:
-1. **先逐点比对差异,列成表**(哪些字段是成员变量、哪些是局部变量;查询列清单;是否有 if 分支;调用点有几处)。
-2. **只搬「这次的逻辑」,保留目标窗体自己的写法**。例:`OQC_PlanMaintain_Cust` 的 rate 查询有 `pcd_custqualmethod` 列且分 `-1`/`-2` 赋值;
-   `_NEW` 只 3 列、无条件赋值 → 移植后再包一层原样赋值,不能把 `-1/-2` 分支也搬过去。
-3. ⚠️ **不要为目标窗体「补」源窗体有、而它本来就不需要的成员字段**——尤其当目标窗体里已有**同名局部变量**时
-   (如 `_NEW` 的 `SaveData_Click` / `ReturnStep` 段落里本就有 `string ms_craftcode = ...`)。
-   C# 允许局部变量遮蔽(shadow)成员字段,编译不报错,但之后排查「这个值到底改的是哪个」会非常痛苦。
-4. **源窗体里被注释掉的代码不要顺手「恢复」**(如 `_NEW` 的 `//pr_code.Text = dt.Rows[0]["PR_QUALMETHOD"]...`),
-   那多半是业务上故意关掉的。
-5. **若目标窗体的某个调用点原本没有这段查询**,判断补上是否算「改动其他逻辑」:
-   若新增的方法调用**不改变任何既有分支的结果**(只是让取值更准确/更新),则属于本次逻辑的一部分,可以补;
-   补了要在交付说明里**单独点出来**,让用户知情。
-6. 同步后对两处(或多处)**重复的同一段查询**保持一致性,日后改动要「几处都改」——
-
-## 数据访问
-- `dh.ExecuteSql(sql, "select")` 返回 `DataTable`;`dh.getFieldDataByCondition(表, 字段, 条件)` 返回单值。
-  - **`字段` 参数可传聚合表达式**(如 `"max(sed_emname)"`),一次查询同时完成「校验存在」+「取名称」:
-    无记录时返回 `DBNull`,`DBNull.ToString() == ""`,判空即可覆盖两种情况,可替代 `CheckExist`。
-- 表命名:工序表 `step`(`st_code`/`st_name`),工序类型表 `steptype`(`STP_CODE`/`STP_NAME`,注意是 STP_ 前缀)。
-- 员工上下线表 `loadman`:`LM_ID`(`loadman_seq.nextval`)/`LM_LINECODE`/`LM_EMCODE`/`LM_UPTIME`/`LM_DOWNTIME`/`LM_INMAN`;
-  `LM_EMNAME` 列仍存在但 **`Query_LoadMan` 已不再写入**(只记工号)。
-  同构的 `loadmake`(工单上下线)有现成界面 `Query_LoadMake` 可作模板。在线判定 = `lm_downtime is null`。
-- 人员名称来源 `SOPEMPLOYEEDETAIL`(`sed_emcode`/`sed_emname`):**同一 `sed_emcode` 可能多条**,取名称统一用 `max(sed_emname)` + `group by sed_emcode` 去重。
-- SQL 多为拼字符串 + Oracle 函数(`nvl`、`ROWNUM`、`DUAL`)。
-- **操作日志表 `commandlog`**:统一调 `LogicHandler.DoCommandLog(caller, userCode, makeCode, lineCode, sourceCode, operate, result, sncode, checkno)`(`UAS_MES_NEW.PublicMethod`),内部固定 insert + `commandlog_seq.nextval` + `sysdate`,调用方不传 ID/时间。惯例:`Tag.ToString(), User.UserCode, 工单号, User.UserLineCode, User.UserSourceCode, "操作名", "结果", sncode, ""`。
+## 技术栈/入口
+- WinForms,.NET Framework,老式 csproj + packages.config;Oracle;主程序集 `UAS_MES_NEW.exe`。
+- Program.cs:非管理员走 UAC 重启;实例数限制 `MaxInstanceCount=3` 的检测必须放管理员分支内、Application.Run 之前。
+- 新增界面三件套(Xxx.cs/Designer.cs/resx,resx 可抄空模板如 Make_SplitBoard.resx)+ csproj 三处注册(Compile/Compile+DependentUpon/EmbeddedResource)。
+- 菜单由库表驱动:`CS$SYSNAVATION`(sn_classname=模块.类名,sn_caller=权限 Caller)+ `CS$USERGROUPPOWER`;实际生效的是 `NavagationBar`(非 AccordionMenu);运行时反射开窗并会**用 sn_caller 覆盖 Designer 的 Tag** → DoCommandLog 记的是 sn_caller。
+
+## 界面通用写法
+- 字段 `asc`/`dh = SystemInf.dh`;Load:`asc.controllInitializeSize(this)` → 初始化 → Focus;SizeChanged:`asc.controlAutoSize(this)`。
+- 日志:`OperateResult.AppendText(">>提示\n", Color.Black)` / ("<<错误\n", Color.Red, 控件)。
+- 自定义控件:SnCollectionBox(SN 输入)、SearchTextBox(放大镜)、MaCodeSearchTextBox(工单)、NormalButton、ChooseAllButton、RichTextAutoBottom、PrinterCombox、ComboxWithTip、AutoSizeFormClass。
+- 常用:`BaseUtil.FillDgvWithDataTable/SetFormValue/CleanDGVData`;`LogicHandler.DoCommandLog(caller,userCode,makeCode,lineCode,sourceCode,操作,结果,sncode,"")`。
+
+## 数据访问/表
+- `dh.ExecuteSql(sql,"select/insert/update/delete")`;`dh.getFieldDataByCondition(表,字段,条件)`——**字段可传聚合**如 `max(sed_emname)`,无记录返回 DBNull.ToString()=="",可一次完成「校验存在+取值」。
+- `loadman`:`lm_id`(loadman_seq.nextval)/lm_linecode/lm_emcode/lm_emname/lm_uptime/lm_downtime/lm_inman;在线判定 `lm_downtime is null`。同构 `loadmake` 参考 `Query_LoadMake`。
+  `Query_LoadMan` 上线时查 `SOPEMPLOYEEDETAIL` 有姓名则写入 `lm_emname`,没有只记工号、不卡控(2026-10-27 定稿:既不解析「工号/姓名」格式,也不做存在性校验)。
+- `SOPEMPLOYEEDETAIL`(sed_emcode/sed_emname):同一工号可能多条,取名称用 `max(sed_emname)`。
+- 工艺三表:`craft`(cr_id/cr_code/cr_prodcode) + `craftdetail`(cd_crid/cd_stepcode/cd_steptype) + `makeserial`(ms_craftcode/ms_prodcode/ms_stepcode);追加 join 必须 left join 否则缺行静默。
+- `productcheckrate`+`PRODUCTCHECKRATEDETAIL`(pcd_pcrid=pcr_id):pcd_stepcode/pcd_steptype/pcd_qualmethod/pcd_custqualmethod/pcd_okretuenstep/pcd_ngretuenstep。
+- 查配置表用「具体条件查一次→查不到放宽重查」两段式,不用 or;**拼字段条件前先判该字段非空串**(空串会拼出 `x=''` 误命中/误回退)。
+
+## 关键坑(血泪)
+- ⚠️ **公共控件(CustomControl/)不许改**,只在功能界面内解决。SnCollectionBox 回车会 SelectAll 覆盖输入 → 界面 hook KeyPress 吞 `\r`(控件只挂 KeyDown)+ KeyDown 双保险 + `_busy` 防重入;事件签名参数类型错会静默不触发。
+- **ChooseAllButton 内部私有 AllChecked 无法复位** → 界面不调 `ChooseAll(dgv)`,自己 Click += 本地处理器 + 本地 `_allChecked`,抽出 `ResetChooseAllState()` 在所有清空点调用。
+- DGV 复选框判断用 `FormattedValue.ToString()=="True"`(.Value 可能 null);全选后 `RefreshEdit()`;批量更新按主键逐条 try/catch;**删除列改 3 处**(AddRange/属性块/字段声明 + .cs 里 Cells["列名"] 引用)。
+- **手工 Rows.Add 前必须先解 DataSource**(FillDgvWithDataTable 会设 DataSource,`Rows.Add` 会抛 InvalidOperationException):`if (dgv.DataSource!=null){dgv.DataSource=null;dgv.Rows.Clear();}`。
+- AutoSize=true 控件(RadioButton)Size 不生效,找位置按「X+文本宽度」估右边界。
+- 同族窗体(_Cust/_NEW)移植逻辑:先逐点比差异(成员 vs 局部变量、列清单、分支),只搬本次逻辑;勿补源有目标无的字段(防局部变量遮蔽);被注释的代码不顺手恢复;新增调用若不改变既有分支结果可补但须向用户点明。
+- Edit 的 old/new 末尾换行必须一致,否则方法签名和 `{` 挤一行。
 
 ## 已知环境限制
-- 无法本地编译验证:`dotnet build` 卡在 NuGet restore;沙箱拦截 MSBuild.exe / csc.exe。
-- 改代码后需在 Visual Studio 中手动编译确认。
-- 自检手段:用 Python 剥离注释与字符串字面量后统计 `{}`/`()`/`[]` 平衡(脚本临时写到 `.workbuddy/`,用完删除)。
-- ⚠️ Edit 教训:`old_string` / `new_string` 的**末尾换行必须一致**,否则会把下一个方法签名和 `{` 挤到同一行(曾误伤 `UpLoadMan_CheckedChanged`)。
-- ⚠️ 本机 `Grep` 工具对 `.Designer.cs` 里的中文注释行匹配不稳,要精确查看某段代码时改用 Python 逐行 dump 更可靠。
-- ⚠️ `git` 不在 PATH 里,须用全路径 `C:\Program Files\Git\cmd\git.exe`(Python `subprocess` 传 `cwd=` 比 `cmd /c cd /d` 稳);
-  `cmd /c` 的输出是 **GBK**,Python 里要 `decode('gbk','replace')`,否则 `UnicodeDecodeError`。
-- 改动是否「零侵入」可以用 `git --no-pager diff --stat -- <path>` 快速自证:文件不出现在输出里 = 完全未改。
+- **无法本地编译**(沙箱拦 MSBuild/csc),改完让用户在 VS 编译。自检:Python 剥注释/字符串后统计 {} () [] 平衡。
+- 本机 Git Bash coreutils 全挂(ls/cp/git 无命令);文件操作用 Read/Write/Edit/Glob/Grep;**git 须全路径** `C:\Program Files\Git\cmd\git.exe`(Python subprocess 传 cwd),`cmd /c` 输出是 GBK。
+- `git diff --stat -- <path>` 自证零侵入。
 
 ## 版本控制(git)
-- 仓库根在 **`C:\MES_NEW\mes-client`**(UAS_MES_YDCY 只是其中一个项目);远程 `mes-client` = `git@repos.ubtob.net:usoft/mes-client.git`(SSH,内网 10.1.81.211),分支 `master`,无 upstream 追踪。
-- `~/.ssh/id_rsa` **带 passphrase**;非交互环境下 SSH 会去读 tty 导致挂起,必须用 askpass 自动应答(OpenSSH 10.3 支持 `SSH_ASKPASS_REQUIRE=force`):
-  ```bash
-  D=$(mktemp -d); printf '#!/bin/sh\necho "<口令>"\n' > "$D/a.sh"; chmod +x "$D/a.sh"
-  export SSH_ASKPASS="$D/a.sh" SSH_ASKPASS_REQUIRE=force DISPLAY=:0
-  git push mes-client master < /dev/null
-  rm -rf "$D"
-  ```
-  备选:`cp ~/.ssh/id_rsa /tmp/k && ssh-keygen -p -P "<口令>" -N "" -f /tmp/k` 造无口令副本 + `GIT_SSH_COMMAND="ssh -i /tmp/k -o IdentitiesOnly=yes"`,**用完立即删除**。
-- 口令一律由用户口头提供,不落盘。
-- 提交前务必限定路径(如 `git add UAS_MES_YDCY/`)——同一仓库里还有 UAS_MES_ZKLH、UAS_WinForm.sln 等并行的他人/其他改动。
+- 仓库根 `C:\MES_NEW\mes-client`;远程 `mes-client` = git@repos.ubtob.net:usoft/mes-client.git(SSH 内网),分支 master。
+- `~/.ssh/id_rsa` 带 passphrase:用 askpass 自动应答(SSH_ASKPASS_REQUIRE=force + 临时 echo 口令脚本,用完删);口令由用户口头提供不落盘。
+- 提交限定路径 `git add UAS_MES_YDCY/`——仓库里还有其他项目/他人的改动。

+ 6 - 6
UAS_MES_YDCY/FunctionCode/Make/Make_ColorBoxWeigh.cs

@@ -146,12 +146,12 @@ namespace UAS_MES_NEW.Make
                     }
                     if (LogicHandler.CheckStepSNAndMacode(ma_code.Text, User.UserSourceCode, sncode.Text, User.UserCode, out oMakeCode, out oMsID, out ErrorMessage))
                     {
-                        //string nextstepcode = dh.getFieldDataByCondition("makeserial", "ms_nextstepcode", "ms_id='" + oMsID + "'").ToString();
-                        //if (nextstepcode != User.CurrentStepCode)
-                        //{
-                        //    OperateResult.AppendText("<<序列号:" + sncode.Text + "下一工序是" + nextstepcode + ",不是当前岗位的工序\n", Color.Red, sncode);
-                        //    return;
-                        //}
+                        string nextstepcode = dh.getFieldDataByCondition("makeserial", "ms_nextstepcode", "ms_id='" + oMsID + "'").ToString();
+                        if (nextstepcode != User.CurrentStepCode)
+                        {
+                            OperateResult.AppendText("<<序列号:" + sncode.Text + "下一工序是" + nextstepcode + ",不是当前岗位的工序\n", Color.Red, sncode);
+                            return;
+                        }
                         sql.Clear();
                         sql.Append("select ms_makecode ma_code,ma_qty,ma_salecode,pr_detail pr_spec,pr_colorboxunit,pr_code,pr_colorboxgw,");
                         sql.Append("pr_colorboxunit,pr_colorboxmaxw,pr_colorboxminw,nvl(PR_CHECKCOLORBOXW,'0') PR_CHECKCOLORBOXW,nvl(pr_sendchecktype,'LineCode')");

+ 6 - 6
UAS_MES_YDCY/FunctionCode/Make/Make_ColorBoxWeigh_HUAGUAN.cs

@@ -148,12 +148,12 @@ namespace UAS_MES_NEW.Make
                     }
                     if (LogicHandler.CheckStepSNAndMacode(ma_code.Text, User.UserSourceCode, sncode.Text, User.UserCode, out oMakeCode, out oMsID, out ErrorMessage))
                     {
-                        //string nextstepcode = dh.getFieldDataByCondition("makeserial", "ms_nextstepcode", "ms_id='" + oMsID + "'").ToString();
-                        //if (nextstepcode != User.CurrentStepCode)
-                        //{
-                        //    OperateResult.AppendText("<<序列号:" + sncode.Text + "下一工序是" + nextstepcode + ",不是当前岗位的工序\n", Color.Red, sncode);
-                        //    return;
-                        //}
+                        string nextstepcode = dh.getFieldDataByCondition("makeserial", "ms_nextstepcode", "ms_id='" + oMsID + "'").ToString();
+                        if (nextstepcode != User.CurrentStepCode)
+                        {
+                            OperateResult.AppendText("<<序列号:" + sncode.Text + "下一工序是" + nextstepcode + ",不是当前岗位的工序\n", Color.Red, sncode);
+                            return;
+                        }
                         sql.Clear();
                         sql.Append("select ms_makecode ma_code,ma_qty,ma_salecode,pr_detail pr_spec,pr_colorboxunit,pr_code,pr_colorboxgw,");
                         sql.Append("pr_colorboxunit,pr_colorboxmaxw,pr_colorboxminw,nvl(PR_CHECKCOLORBOXW,'0') PR_CHECKCOLORBOXW,nvl(pr_sendchecktype,'LineCode')");

+ 6 - 6
UAS_MES_YDCY/FunctionCode/Make/Make_ColorBoxWeigh_LANCHEN.cs

@@ -133,12 +133,12 @@ namespace UAS_MES_NEW.Make
                     string oMsID = "";
                     if (LogicHandler.CheckStepSNAndMacode(ma_code.Text, User.UserSourceCode, sncode.Text, User.UserCode, out oMakeCode, out oMsID, out ErrorMessage))
                     {
-                        //string nextstepcode = dh.getFieldDataByCondition("makeserial", "ms_nextstepcode", "ms_id='" + oMsID + "'").ToString();
-                        //if (nextstepcode != User.CurrentStepCode)
-                        //{
-                        //    OperateResult.AppendText("<<序列号:" + sncode.Text + "下一工序是" + nextstepcode + ",不是当前岗位的工序\n", Color.Red, sncode);
-                        //    return;
-                        //}
+                        string nextstepcode = dh.getFieldDataByCondition("makeserial", "ms_nextstepcode", "ms_id='" + oMsID + "'").ToString();
+                        if (nextstepcode != User.CurrentStepCode)
+                        {
+                            OperateResult.AppendText("<<序列号:" + sncode.Text + "下一工序是" + nextstepcode + ",不是当前岗位的工序\n", Color.Red, sncode);
+                            return;
+                        }
                         sql.Clear();
                         sql.Append("select ms_makecode ma_code,ma_qty,ma_salecode,pr_detail pr_spec,pr_colorboxunit,pr_code,pr_colorboxgw,");
                         sql.Append("pr_colorboxunit,pr_colorboxmaxw,pr_colorboxminw,nvl(PR_CHECKCOLORBOXW,'0') PR_CHECKCOLORBOXW,nvl(pr_sendchecktype,'LineCode')");

+ 14 - 2
UAS_MES_YDCY/FunctionCode/Query/Query_LoadMan.Designer.cs

@@ -35,6 +35,7 @@ namespace UAS_MES_NEW.Query
             this.choose = new System.Windows.Forms.DataGridViewCheckBoxColumn();
             this.lm_id = new System.Windows.Forms.DataGridViewTextBoxColumn();
             this.lm_emcode = new System.Windows.Forms.DataGridViewTextBoxColumn();
+            this.lm_emname = new System.Windows.Forms.DataGridViewTextBoxColumn();
             this.lm_linecode = new System.Windows.Forms.DataGridViewTextBoxColumn();
             this.lm_uptime = new System.Windows.Forms.DataGridViewTextBoxColumn();
             this.lm_inman = new System.Windows.Forms.DataGridViewTextBoxColumn();
@@ -78,6 +79,7 @@ namespace UAS_MES_NEW.Query
             this.choose,
             this.lm_id,
             this.lm_emcode,
+            this.lm_emname,
             this.lm_linecode,
             this.lm_uptime,
             this.lm_inman});
@@ -114,9 +116,18 @@ namespace UAS_MES_NEW.Query
             this.lm_emcode.Name = "lm_emcode";
             this.lm_emcode.ReadOnly = true;
             this.lm_emcode.Width = 180;
-            // 
+            //
+            // lm_emname
+            //
+            this.lm_emname.DataPropertyName = "lm_emname";
+            this.lm_emname.HeaderText = "人员名称";
+            this.lm_emname.MinimumWidth = 10;
+            this.lm_emname.Name = "lm_emname";
+            this.lm_emname.ReadOnly = true;
+            this.lm_emname.Width = 120;
+            //
             // lm_linecode
-            // 
+            //
             this.lm_linecode.DataPropertyName = "lm_linecode";
             this.lm_linecode.HeaderText = "线别";
             this.lm_linecode.MinimumWidth = 10;
@@ -300,6 +311,7 @@ namespace UAS_MES_NEW.Query
         private System.Windows.Forms.DataGridViewCheckBoxColumn choose;
         private System.Windows.Forms.DataGridViewTextBoxColumn lm_id;
         private System.Windows.Forms.DataGridViewTextBoxColumn lm_emcode;
+        private System.Windows.Forms.DataGridViewTextBoxColumn lm_emname;
         private System.Windows.Forms.DataGridViewTextBoxColumn lm_linecode;
         private System.Windows.Forms.DataGridViewTextBoxColumn lm_uptime;
         private System.Windows.Forms.DataGridViewTextBoxColumn lm_inman;

+ 17 - 6
UAS_MES_YDCY/FunctionCode/Query/Query_LoadMan.cs

@@ -54,7 +54,7 @@ namespace UAS_MES_NEW.Query
 
         /// <summary>
         /// 人员工号输入框,回车后按选中的模式执行上线/下线
-        /// 只接受工号,不解析姓名,也不校验人员是否存在于 SOPEMPLOYEEDETAIL
+        /// 只接受工号,不解析姓名;上线时若工号存在于 SOPEMPLOYEEDETAIL 则顺带记录人员名称,不存在则只记工号,不做存在性卡控
         /// </summary>
         private void em_code_KeyDown(object sender, KeyEventArgs e)
         {
@@ -138,9 +138,19 @@ namespace UAS_MES_NEW.Query
                     dh.ExecuteSql("update loadman set lm_downtime=sysdate where lm_emcode='" + emcode + "' and lm_downtime is null", "update");
                     OperateResult.AppendText(">>" + emcode + "已从" + oldLineCode + "自动下线\n", Color.Black);
                 }
-                dh.ExecuteSql("insert into loadman(lm_id,lm_linecode,lm_emcode,lm_uptime,lm_inman)" +
-                    "values(loadman_seq.nextval,'" + linecode + "','" + emcode + "',sysdate,'" + User.UserName + "')", "insert");
-                OperateResult.AppendText("<<" + emcode + " 在" + linecode + "上线成功\n", Color.Green, em_code);
+                //人员名称:存在于 SOPEMPLOYEEDETAIL 则带上,不存在只记工号,不做存在性卡控
+                string emname = dh.getFieldDataByCondition("SOPEMPLOYEEDETAIL", "max(sed_emname)", "sed_emcode='" + emcode + "'").ToString().Trim();
+                if (emname == "")
+                {
+                    dh.ExecuteSql("insert into loadman(lm_id,lm_linecode,lm_emcode,lm_uptime,lm_inman)" +
+                        "values(loadman_seq.nextval,'" + linecode + "','" + emcode + "',sysdate,'" + User.UserName + "')", "insert");
+                }
+                else
+                {
+                    dh.ExecuteSql("insert into loadman(lm_id,lm_linecode,lm_emcode,lm_emname,lm_uptime,lm_inman)" +
+                        "values(loadman_seq.nextval,'" + linecode + "','" + emcode + "','" + emname + "',sysdate,'" + User.UserName + "')", "insert");
+                }
+                OperateResult.AppendText("<<" + emcode + (emname == "" ? "" : " " + emname) + " 在" + linecode + "上线成功\n", Color.Green, em_code);
             }
             else
             {
@@ -276,11 +286,12 @@ namespace UAS_MES_NEW.Query
 
         /// <summary>
         /// 刷新DGV:只取未下线的人员,已选线别时只显示当前线别
-        /// 只显示工号,不关联 SOPEMPLOYEEDETAIL(上线时间 to_char 到秒)
+        /// 人员名称取 loadman.lm_emname(上线时查 SOPEMPLOYEEDETAIL 带出,查不到的记录为空)
+        /// (上线时间 to_char 到秒)
         /// </summary>
         private void LoadGridData()
         {
-            string sql = "select lm.lm_id,lm.lm_linecode,lm.lm_emcode,"
+            string sql = "select lm.lm_id,lm.lm_linecode,lm.lm_emcode,lm.lm_emname,"
                 + "to_char(lm.lm_uptime,'yyyy-mm-dd hh24:mi:ss') lm_uptime,lm.lm_inman from loadman lm"
                 + " where lm.lm_downtime is null";
             if (li_code.Text != "")