Преглед изворни кода

G3-1 operate 逐方法埋点:C# 端 M8 操作日志铺开(VM 层 + View code-behind 命令入口)

VM 层:HouseDebugPageVM ~23 法(LED/进排气阀/补排气/写EEPROM/水平垂直
电机各向/抓图/一键电机准备/手调保存)、BufferDebugVM 3、MainPageVM 2
(结束培养/报警静音)、UnifiedConfigVM 1(保存配置,排除密码)。

View code-behind 层(真实操作所在,MVVM 不规范):加皿窗口(编辑保存/
启动培养皿/启动平衡皿)、胚胎详情(移动/释放/删除/作废/结束)、舱室设置
(保存舱室/系统设置)、对焦设置(保存/批量校验)。

事件处理器用 OperationLogger.Begin scope + 失败路径显式 Fail()
(Dispose 默认记成功),不改控制流;VM 同步动作用 Run 自动计时+异常。
HTTP/串口/相机已 P3b 单点收口覆盖;展示类/纯数据 VM 正确跳过。
operate dotnet build 0 error。真入 operation_log 待起 Kafka+oplog 运行抽查。

文档同步:进度状态.yaml/交接卡.md/工作计划表.md/当前开发计划.md/进度数据.js。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie пре 5 дана
родитељ
комит
791a76d6f1

+ 36 - 0
ivf_tl_operate_2.0/ivf_tl_Operate/View/AddDishWindowView.xaml.cs

@@ -284,6 +284,9 @@ namespace ivf_tl_Operate.View
         /// <param name="e"></param>
         private void EditDishSave_Click(object sender, RoutedEventArgs e)
         {
+            // M8-G3-1:命令入口埋点(module=皿管理)。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("皿管理", "保存/编辑皿信息");
+            _op.Input(new { dishId = ExDish?.id, houseSn = ExDish?.houseSn, caseId = ExDish?.caseId });
             try
             {
                 List<int> wellList = new List<int>();
@@ -302,6 +305,7 @@ namespace ivf_tl_Operate.View
                 string caseIdString = this._caseId_TextBox.Text.Trim();
                 if (string.IsNullOrEmpty(caseIdString))
                 {
+                    _op.Fail("校验失败:病例编号为空"); // M8-G3-1
                     //MessageInfo("请输入病例编号");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0148"));
                     return;
@@ -310,6 +314,7 @@ namespace ivf_tl_Operate.View
                 string cycleIdString = this._cycle_TextBox.Text.Trim();
                 if (string.IsNullOrEmpty(cycleIdString))
                 {
+                    _op.Fail("校验失败:周期编号为空"); // M8-G3-1
                     //MessageInfo("请输入周期编号");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0316"));
                     return;
@@ -318,6 +323,7 @@ namespace ivf_tl_Operate.View
                 string wifeString = this._wife_TextBox.Text.Trim();
                 if (string.IsNullOrEmpty(wifeString))
                 {
+                    _op.Fail("校验失败:女方姓名为空"); // M8-G3-1
                     //MessageInfo("请输入女方姓名");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0149"));
                     return;
@@ -339,6 +345,7 @@ namespace ivf_tl_Operate.View
                     if (DateTime.TryParse($"{this._wifeYear.SelectedItem}-{this._wifeMonth.SelectedItem}-{this._wifeDay.SelectedItem}", out DateTime wifeTime)) wifeBirthString = wifeTime.ToString("yyyy-MM-dd HH:mm:ss");
                     else
                     {
+                        _op.Fail("校验失败:女方年龄错误"); // M8-G3-1
                         MessageInfo("女方年龄错误");
                         return;
                     }
@@ -346,6 +353,7 @@ namespace ivf_tl_Operate.View
                 string husbandString = this._husband_TextBox.Text.Trim();
                 if (string.IsNullOrEmpty(husbandString))
                 {
+                    _op.Fail("校验失败:男方姓名为空"); // M8-G3-1
                     //MessageInfo("请输入男方姓名");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0150"));
                     return;
@@ -367,6 +375,7 @@ namespace ivf_tl_Operate.View
                     if (DateTime.TryParse($"{this._husbandYear.SelectedItem}-{this._husbandMonth.SelectedItem}-{(int)this._husbandDay.SelectedItem}", out DateTime husTime)) husbandBirthString = husTime.ToString("yyyy-MM-dd HH:mm:ss");
                     else
                     {
+                        _op.Fail("校验失败:男方年龄错误"); // M8-G3-1
                         MessageInfo("男方年龄错误");
                         return;
                     }
@@ -395,17 +404,20 @@ namespace ivf_tl_Operate.View
 
                 if (!AppData.Instance.HttpHelper.UpdateDishInfoApi(body))
                 {
+                    _op.Fail("修改皿信息接口返回失败"); // M8-G3-1
                     //MessageInfo("修改信息失败");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0243"));
                 }
                 else
                 {
+                    _op.Success(); // M8-G3-1
                     this.DialogResult = true;
 
                 }
             }
             catch (Exception ex)
             {
+                _op.Fail(ex.GetType().Name + ": " + ex.Message); // M8-G3-1
                 AppData.Instance.LogHelper.ExceptionLog(ex, "AddDishWindowView.EditDishSave_Click", LogEnum.RunException);
             }
         }
@@ -417,6 +429,9 @@ namespace ivf_tl_Operate.View
         /// <param name="e"></param>
         private void StartDish_Click(object sender, RoutedEventArgs e)
         {
+            // M8-G3-1:命令入口埋点(module=皿管理)。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("皿管理", "启动培养皿");
+            _op.Input(new { houseSn = housesn, tlSn = tlsn });
             try
             {
                 List<int> wellList = new List<int>();
@@ -431,6 +446,7 @@ namespace ivf_tl_Operate.View
                 }
                 if (!wellList.Any())
                 {
+                    _op.Fail("校验失败:未选择胚胎孔位"); // M8-G3-1
                     //MessageInfo("请选择胚胎孔位");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0317"));
                     return;
@@ -438,6 +454,7 @@ namespace ivf_tl_Operate.View
                 string caseIdString = this._caseId_TextBox.Text.Trim();
                 if (string.IsNullOrEmpty(caseIdString))
                 {
+                    _op.Fail("校验失败:病例编号为空"); // M8-G3-1
                     //MessageInfo("请输入病例编号");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0148"));
                     return;
@@ -445,6 +462,7 @@ namespace ivf_tl_Operate.View
                 string cycleIdString = this._cycle_TextBox.Text.Trim();
                 if (string.IsNullOrEmpty(cycleIdString))
                 {
+                    _op.Fail("校验失败:周期编号为空"); // M8-G3-1
                     //MessageInfo("请输入周期编号");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0316"));
                     return;
@@ -452,6 +470,7 @@ namespace ivf_tl_Operate.View
                 string wifeString = this._wife_TextBox.Text.Trim();
                 if (string.IsNullOrEmpty(wifeString))
                 {
+                    _op.Fail("校验失败:女方姓名为空"); // M8-G3-1
                     //MessageInfo("请输入女方姓名");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0149"));
                     return;
@@ -473,6 +492,7 @@ namespace ivf_tl_Operate.View
                     if (DateTime.TryParse($"{this._wifeYear.SelectedItem}-{this._wifeMonth.SelectedItem}-{this._wifeDay.SelectedItem}", out DateTime wifeTime)) wifeBirthString = wifeTime.ToString("yyyy-MM-dd HH:mm:ss");
                     else
                     {
+                        _op.Fail("校验失败:女方年龄错误"); // M8-G3-1
                         MessageInfo("女方年龄错误");
                         return;
                     }
@@ -480,6 +500,7 @@ namespace ivf_tl_Operate.View
                 string husbandString = this._husband_TextBox.Text.Trim();
                 if (string.IsNullOrEmpty(husbandString))
                 {
+                    _op.Fail("校验失败:男方姓名为空"); // M8-G3-1
                     //MessageInfo("请输入男方姓名");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0150"));
                     return;
@@ -501,6 +522,7 @@ namespace ivf_tl_Operate.View
                     if (DateTime.TryParse($"{this._husbandYear.SelectedItem}-{this._husbandMonth.SelectedItem}-{(int)this._husbandDay.SelectedItem}", out DateTime husTime)) husbandBirthString = husTime.ToString("yyyy-MM-dd HH:mm:ss");
                     else
                     {
+                        _op.Fail("校验失败:男方年龄错误"); // M8-G3-1
                         MessageInfo("男方年龄错误");
                         return;
                     }
@@ -508,6 +530,7 @@ namespace ivf_tl_Operate.View
                 string startTimeString = this._startTime_TextBox.Text.Trim();
                 if (string.IsNullOrEmpty(startTimeString))
                 {
+                    _op.Fail("校验失败:受精时间为空"); // M8-G3-1
                     //MessageInfo("请输入受精时间");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0154"));
                     return;
@@ -517,6 +540,7 @@ namespace ivf_tl_Operate.View
                 {
                     if (result1 > DateTime.Now)
                     {
+                        _op.Fail("校验失败:受精时间超过当前时间"); // M8-G3-1
                         //MessageInfo("受精时间不能超过当前时间");
                         MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0264"));
                         return;
@@ -524,6 +548,7 @@ namespace ivf_tl_Operate.View
                 }
                 else
                 {
+                    _op.Fail("校验失败:受精时间输入错误"); // M8-G3-1
                     //MessageInfo("受精时间输入错误");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0154"));
                     return;
@@ -568,16 +593,19 @@ namespace ivf_tl_Operate.View
                 string error = AppData.Instance.HttpHelper.AddCultureRecordApi(exDish);
                 if (!string.IsNullOrEmpty(error))
                 {
+                    _op.Fail("启动培养皿接口返回失败:" + error); // M8-G3-1
                     //MessageInfo($"开始培养失败:{error}");
                     MessageInfo($"{KeyToStringConvert.GetLanguageStringByKey("C0265")}:{error}");
                 }
                 else
                 {
+                    _op.Success(); // M8-G3-1
                     this.DialogResult = true;
                 }
             }
             catch (Exception ex)
             {
+                _op.Fail(ex.GetType().Name + ": " + ex.Message); // M8-G3-1
                 AppData.Instance.LogHelper.ExceptionLog(ex, "AddDishWindowView.StartDish_Click", LogEnum.RunException);
             }
             e.Handled = true;
@@ -591,20 +619,26 @@ namespace ivf_tl_Operate.View
         private void StartBalance_Click(object sender, RoutedEventArgs e)
         {
             //Debug.WriteLine("平衡按钮");
+            // M8-G3-1:命令入口埋点(module=皿管理)。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("皿管理", isBalacne ? "结束平衡皿" : "启动平衡皿");
+            _op.Input(new { houseSn = housesn, tlSn = tlsn, isBalance = isBalacne, balanceId = balanceId });
             if (isBalacne)//结束平衡
             {
                 if (balanceId == 0)
                 {
+                    _op.Fail("校验失败:获取平衡记录失败"); // M8-G3-1
                     //MessageInfo("获取平衡记录失败");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0266"));
                     return;
                 }
                 if (AppData.Instance.HttpHelper.StopBalanceApi(balanceId))
                 {
+                    _op.Success(); // M8-G3-1
                     this.DialogResult = true;
                 }
                 else
                 {
+                    _op.Fail("结束平衡接口返回失败"); // M8-G3-1
                     //MessageInfo("结束平衡失败");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0267"));
                 }
@@ -613,10 +647,12 @@ namespace ivf_tl_Operate.View
             {
                 if (AppData.Instance.HttpHelper.StartBalanceApi(housesn, tlsn))
                 {
+                    _op.Success(); // M8-G3-1
                     this.DialogResult = true;
                 }
                 else
                 {
+                    _op.Fail("开始平衡接口返回失败"); // M8-G3-1
                     //MessageInfo("开始平衡失败");
                     MessageInfo(KeyToStringConvert.GetLanguageStringByKey("C0268"));
                 }

+ 37 - 2
ivf_tl_operate_2.0/ivf_tl_Operate/View/AutoFocusSettingView.xaml.cs

@@ -49,14 +49,26 @@ namespace ivf_tl_Operate.View
                 new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0259"));
                 return;
             }
+            // M8-G3-1:批量校验/赋值对焦设置命令入口埋点。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("对焦设置", "批量校验对焦设置");
+            _op.Input(new
+            {
+                输入值 = newValueString,
+                应用对焦次数 = this._newAutoFocus_CheckBox.IsChecked == true,
+                应用更新对焦起点 = this._updateFocusNumber_CheckBox.IsChecked == true,
+                应用更新拍照位置 = this._updateClearestNumber_CheckBox.IsChecked == true,
+                舱室总数 = vm?.HouseInfoList?.Count ?? 0
+            });
+            try
+            {
             if (int.TryParse(newValueString, out int newValue))
             {
                 foreach (var item in vm.HouseInfoList)
                 {
                     if (this._newAutoFocus_CheckBox.IsChecked == true) item.NewAutoFocusNumber = newValue;
-                    if (this._updateFocusNumber_CheckBox.IsChecked == true) 
+                    if (this._updateFocusNumber_CheckBox.IsChecked == true)
                         item.NewUpdateFocusNumber = newValue;
-                    if (this._updateClearestNumber_CheckBox.IsChecked == true) 
+                    if (this._updateClearestNumber_CheckBox.IsChecked == true)
                         item.NewUpdateClearestNumber = newValue;
                 }
                 if (this._newAutoFocus_CheckBox.IsChecked == true)
@@ -65,17 +77,30 @@ namespace ivf_tl_Operate.View
                     this._updateFocusNumber_CheckBox.IsChecked = false;
                 if (this._updateClearestNumber_CheckBox.IsChecked == true)
                     this._updateClearestNumber_CheckBox.IsChecked = false;
+                _op.Success();
             }
             else
             {
                 //new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, "请输入正确的数字");
                 new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0259"));
+                _op.Fail("输入值非法,无法解析为整数:" + newValueString);
                 return;
             }
+            }
+            catch (Exception ex)
+            {
+                _op.Fail(ex.GetType().Name + ": " + ex.Message);
+                throw;
+            }
         }
 
         private void SaveHouse_Click(object sender, RoutedEventArgs e)
         {
+            // M8-G3-1:保存对焦设置命令入口埋点。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("对焦设置", "保存对焦设置");
+            _op.Input(new { 舱室总数 = vm?.HouseInfoList?.Count ?? 0 });
+            try
+            {
             bool isUp = false;
             bool isUpHouse = false;
             List<AutoFocusHouseInfoRequest> upList = new List<AutoFocusHouseInfoRequest>();
@@ -115,11 +140,13 @@ namespace ivf_tl_Operate.View
 
                 if (item.updateFocusNumber > item.autoFocusNumber)
                 {
+                    _op.Fail($"{item.houseSn}号舱室校验失败:更新对焦起点不能大于对焦次数");
                     MessageBox.Show($"{item.houseSn}号舱室抠图成功数-更新对焦起点不能大于对焦次数");
                     return;
                 }
                 if (item.updateClearestNumber > item.autoFocusNumber)
                 {
+                    _op.Fail($"{item.houseSn}号舱室校验失败:更新拍照位置不能大于对焦次数");
                     MessageBox.Show($"{item.houseSn}号舱室抠图成功数-更新拍照位置不能大于对焦次数");
                     return;
                 }
@@ -193,11 +220,19 @@ namespace ivf_tl_Operate.View
             {
                 //new MessagePrompt().ShowCenterOwnerSuccess(AppData.Instance.MainWindow, "保存成功");
                 new MessagePrompt().ShowCenterOwnerSuccess(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0242"));
+                _op.Success();
             }
             else
             {
                 //new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, "保存失败");
                 new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0243"));
+                _op.Fail("后端下发保存失败:HouseImmediatelyAPi 返回 false");
+            }
+            }
+            catch (Exception ex)
+            {
+                _op.Fail(ex.GetType().Name + ": " + ex.Message);
+                throw;
             }
         }
 

+ 35 - 7
ivf_tl_operate_2.0/ivf_tl_Operate/View/DetailPageView.xaml.cs

@@ -296,6 +296,9 @@ namespace ivf_tl_Operate.View
             PlayerPause();
             if (vm.CurrentEmbryo == null) return;
 
+            // M8-G3-1:胚胎操作命令入口埋点。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("胚胎操作", "移动胚胎");
+            _op.Input(new { embryoId = vm.CurrentEmbryo.id, wellSn = vm.CurrentEmbryo.wellSn, houseSn = vm.CurrentEmbryo.houseSn, state = vm.CurrentEmbryo.state });
             string tag = KeyToStringConvert.GetLanguageStringByKey("C0136");
             string messInfo = GetMessageInfo(tag);
 
@@ -325,6 +328,7 @@ namespace ivf_tl_Operate.View
                 }
             }
             EmbryoOp(tag, messInfo, stateTag);
+            _op.Success();
             return;
         }
         //private void EmbryoMove_Click(object sender, RoutedEventArgs e)
@@ -367,6 +371,9 @@ namespace ivf_tl_Operate.View
         {
             PlayerPause();
             if (vm.CurrentEmbryo == null) return;
+            // M8-G3-1:胚胎操作命令入口埋点。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("胚胎操作", "释放胚胎");
+            _op.Input(new { embryoId = vm.CurrentEmbryo.id, wellSn = vm.CurrentEmbryo.wellSn, houseSn = vm.CurrentEmbryo.houseSn, state = vm.CurrentEmbryo.state });
             string tag = KeyToStringConvert.GetLanguageStringByKey("C0135");
             string messInfo = GetMessageInfo(tag);
             int stateTag = 2;
@@ -394,6 +401,7 @@ namespace ivf_tl_Operate.View
                 }
             }
             EmbryoOp(tag, messInfo, stateTag);
+            _op.Success();
             return;
         }
         //private void EmbryoFree_Click(object sender, RoutedEventArgs e)
@@ -437,6 +445,9 @@ namespace ivf_tl_Operate.View
             PlayerPause();
             if (vm.CurrentEmbryo == null) return;
 
+            // M8-G3-1:胚胎操作命令入口埋点。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("胚胎操作", "删除胚胎");
+            _op.Input(new { embryoId = vm.CurrentEmbryo.id, wellSn = vm.CurrentEmbryo.wellSn, houseSn = vm.CurrentEmbryo.houseSn, state = vm.CurrentEmbryo.state });
             string tag = KeyToStringConvert.GetLanguageStringByKey("C0137");
             string messInfo = GetMessageInfo(tag);
             int stateTag = 3;
@@ -464,6 +475,7 @@ namespace ivf_tl_Operate.View
                 }
             }
             EmbryoOp(tag, messInfo, stateTag);
+            _op.Success();
             return;
         }
         //private void EmbryoDel_Click(object sender, RoutedEventArgs e)
@@ -507,6 +519,9 @@ namespace ivf_tl_Operate.View
             PlayerPause();
             if (vm.CurrentEmbryo == null) return;
 
+            // M8-G3-1:胚胎操作命令入口埋点。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("胚胎操作", "作废胚胎");
+            _op.Input(new { embryoId = vm.CurrentEmbryo.id, wellSn = vm.CurrentEmbryo.wellSn, houseSn = vm.CurrentEmbryo.houseSn, state = vm.CurrentEmbryo.state });
             string tag = KeyToStringConvert.GetLanguageStringByKey("C0138");
             string messInfo = GetMessageInfo(tag);
             int stateTag = 4;
@@ -534,6 +549,7 @@ namespace ivf_tl_Operate.View
                 }
             }
             EmbryoOp(tag, messInfo, stateTag);
+            _op.Success();
             return;
         }
         //private void EmbryoInv_Click(object sender, RoutedEventArgs e)
@@ -578,15 +594,27 @@ namespace ivf_tl_Operate.View
             {
                 PlayerPause();
                 if (vm.ExDish == null || vm.ExDish.id == 0) return;
-                string tag = KeyToStringConvert.GetLanguageStringByKey("C0313");
-                string messInfo = KeyToStringConvert.GetLanguageStringByKey("C0314");
-                if (vm.ExDish.state == 0)
+                // M8-G3-1:胚胎操作命令入口埋点。
+                using var _op = Aivfo.OperationLog.OperationLogger.Begin("胚胎操作", "结束胚胎培养");
+                _op.Input(new { dishId = vm.ExDish.id, houseSn = vm.ExDish.houseSn, state = vm.ExDish.state });
+                try
                 {
-                    messInfo = KeyToStringConvert.GetLanguageStringByKey("C0315");
-                    tag = "完成";
+                    string tag = KeyToStringConvert.GetLanguageStringByKey("C0313");
+                    string messInfo = KeyToStringConvert.GetLanguageStringByKey("C0314");
+                    if (vm.ExDish.state == 0)
+                    {
+                        messInfo = KeyToStringConvert.GetLanguageStringByKey("C0315");
+                        tag = "完成";
+                    }
+                    EmbryoOp(tag, messInfo, 5);
+                    _op.Success();
+                    return;
+                }
+                catch (Exception)
+                {
+                    _op.Fail("EmbryoOver_Click exception");
+                    throw;
                 }
-                EmbryoOp(tag, messInfo, 5);
-                return;
             }
             catch (Exception ex)
             {

+ 24 - 0
ivf_tl_operate_2.0/ivf_tl_Operate/View/HouseSettingPageView.xaml.cs

@@ -91,13 +91,18 @@ namespace ivf_tl_Operate.View
 
         private void SaveHouse_Click(object sender, RoutedEventArgs e)
         {
+            // M8-G3-1:保存舱室设置命令入口埋点。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("舱室设置", "保存舱室设置");
+            _op.Input(new { houseCount = vm.HouseInfoList?.Count ?? 0 });
             if (AppData.Instance.HttpHelper.HouseUpdateApi(vm.HouseInfoList))
             {
+                _op.Success();
                 //new MessagePrompt().ShowCenterOwnerSuccess(AppData.Instance.MainWindow, "保存成功");
                 new MessagePrompt().ShowCenterOwnerSuccess(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0242"));
             }
             else
             {
+                _op.Fail("HouseUpdateApi 返回 false:保存舱室设置失败");
                 //new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, "保存失败");
                 new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0243"));
             }
@@ -141,11 +146,21 @@ namespace ivf_tl_Operate.View
 
         private void SaveSetting_Click(object sender, RoutedEventArgs e)
         {
+            // M8-G3-1:保存系统设置命令入口埋点(含本地校验计算)。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("舱室设置", "保存系统设置");
+            _op.Input(new
+            {
+                pressureAlarmMax = this.TextBox_pressureAlarmMax.Text?.Trim(),
+                pressureAlarmMin = this.TextBox_pressureAlarmMin.Text?.Trim(),
+                temperatureAlarmMax = this.TextBox_temperatureAlarmMax.Text?.Trim(),
+                temperatureAlarmMin = this.TextBox_temperatureAlarmMin.Text?.Trim()
+            });
             decimal decimalValue;
             if (decimal.TryParse(this.TextBox_pressureAlarmMax.Text.Trim(), out decimalValue))
             {
                 if (decimalValue < 0)
                 {
+                    _op.Fail("校验失败:舱室压力上限小于0");
                     //new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, $"舱室压力上限请输入大于0的整数或者小数");
                     new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0244"));
                     return;
@@ -153,6 +168,7 @@ namespace ivf_tl_Operate.View
             }
             else
             {
+                _op.Fail("校验失败:舱室压力上限非数字");
                 //new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, $"舱室压力上限请输入整数或者小数");
                 new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0244"));
                 return;
@@ -161,6 +177,7 @@ namespace ivf_tl_Operate.View
             {
                 if (decimalValue < 0)
                 {
+                    _op.Fail("校验失败:舱室压力下限小于0");
                     //new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, $"舱室压力下限请输入大于0的整数或者小数");
                     new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0245"));
                     return;
@@ -168,6 +185,7 @@ namespace ivf_tl_Operate.View
             }
             else
             {
+                _op.Fail("校验失败:舱室压力下限非数字");
                 //new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, $"舱室压力下限值请输入整数或者小数");
                 new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0245"));
                 return;
@@ -176,6 +194,7 @@ namespace ivf_tl_Operate.View
             {
                 if (decimalValue < 0)
                 {
+                    _op.Fail("校验失败:舱室温度上限小于0");
                     //new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, $"舱室温度上限请输入大于0的整数或者小数");
                     new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0246"));
                     return;
@@ -183,6 +202,7 @@ namespace ivf_tl_Operate.View
             }
             else
             {
+                _op.Fail("校验失败:舱室温度上限非数字");
                 //new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, $"舱室温度上限请输入整数或者小数");
                 new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0246"));
                 return;
@@ -191,6 +211,7 @@ namespace ivf_tl_Operate.View
             {
                 if (decimalValue < 0)
                 {
+                    _op.Fail("校验失败:舱室温度下限小于0");
                     //new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, $"舱室温度下限请输入大于0的整数或者小数");
                     new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0247"));
                     return;
@@ -198,6 +219,7 @@ namespace ivf_tl_Operate.View
             }
             else
             {
+                _op.Fail("校验失败:舱室温度下限非数字");
                 //new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, $"舱室温度下限请输入整数或者小数");
                 new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0247"));
                 return;
@@ -205,11 +227,13 @@ namespace ivf_tl_Operate.View
             string error = AppData.Instance.HttpHelper.SystemUpdateApi(vm.TLSettingModel);
             if (string.IsNullOrEmpty(error))
             {
+                _op.Success();
                 //new MessagePrompt().ShowCenterOwnerSuccess(AppData.Instance.MainWindow, "保存成功");
                 new MessagePrompt().ShowCenterOwnerSuccess(AppData.Instance.MainWindow, KeyToStringConvert.GetLanguageStringByKey("C0242"));
             }
             else
             {
+                _op.Fail("SystemUpdateApi 失败:" + error);
                 //new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, $"保存失败:{error}");
                 new MessagePrompt().ShowCenterOwnerDefeat(AppData.Instance.MainWindow, $"{KeyToStringConvert.GetLanguageStringByKey("C0243")}:{error}");
             }

+ 12 - 1
ivf_tl_operate_2.0/ivf_tl_Operate/ViewModel/BufferDebugViewModel.cs

@@ -238,7 +238,10 @@ namespace ivf_tl_Operate.ViewModel
         public void Are()
         {
             if (Serial == null) return;
-            Serial.BufferBottleAerationWait();
+            // M8-G3-1:缓冲瓶补气为串口设备命令入口,补操作日志埋点(module=缓冲瓶调试)。
+            Aivfo.OperationLog.OperationLogger.Run("缓冲瓶调试", "缓冲瓶补气",
+                () => Serial.BufferBottleAerationWait(),
+                input: new { houseSn = CurrentHouse?.houseSn });
         }
 
         /// <summary>
@@ -249,6 +252,10 @@ namespace ivf_tl_Operate.ViewModel
         {
             if (Serial == null) return;
             // M1-B2:走 lease.Serial→control ComBin 缓冲瓶进气阀时间写(builder 与 operate 逐字节一致)。⚠ 待真机 V-010。
+            // M8-G3-1:写进气阀时间为"写 EEPROM 串口命令 + HTTP 持久化"打包操作,用 Begin scope 给父操作名串联子埋点(module=缓冲瓶调试)。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("缓冲瓶调试", "保存缓冲瓶进气阀时间",
+                houseSn: CurrentHouse?.houseSn);
+            try { _op.Input(new { houseSn = CurrentHouse?.houseSn, newValue }); } catch { }
             Serial.WriteOpenIntakeTimeWait(newValue, isBuffer: true);
             CurrentHouse.inletValveOpeningTime = newValue;
             AddMessageInfo($"进气阀打开时间保存服务器结果:{AppData.Instance.HttpHelper.LightsUpdateApi(tLSetting.tlSn, LedLight, CurrentHouse.inletValveOpeningTime)}");
@@ -263,6 +270,10 @@ namespace ivf_tl_Operate.ViewModel
             if (Serial == null) return;
             // ⚠ M1-B2:control Commander 缺 CreateWriteEEPROMLightNum(T1.1 核实),不臆造字节 → 暂不下发到下位机。
             // 待真机核对字节后在 control Commander 补 builder 再接入;当前仅本地+服务器记录,未真正写灯光EEPROM。
+            // M8-G3-1:设置灯光亮度为"写 EEPROM 串口命令 + HTTP 持久化"打包操作,用 Begin scope 给父操作名串联子埋点(module=缓冲瓶调试)。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("缓冲瓶调试", "保存缓冲瓶灯光亮度",
+                houseSn: CurrentHouse?.houseSn);
+            try { _op.Input(new { houseSn = CurrentHouse?.houseSn, newValue }); } catch { }
             if (!Serial.WriteLightBrightnessWait(newValue))
             {
                 AddMessageInfo($"[灯光亮度]control 端暂缺写E方命令(待真机补字节),本次未下发到下位机,仅本地暂存{newValue}");

+ 101 - 19
ivf_tl_operate_2.0/ivf_tl_Operate/ViewModel/HouseDebugPageViewModel.cs

@@ -417,7 +417,10 @@ namespace ivf_tl_Operate.ViewModel
         public void OpenLed()
         {
             if (Serial == null) return;
-            Serial.OpenLedWait();
+            // M8-G3-1:LED 开关为串口设备命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "打开LED灯",
+                () => Serial.OpenLedWait(),
+                input: new { houseSn = CurrentHouseId });
             LedState = KeyToStringConvert.GetLanguageStringByKey("C0306");
             //LedState = "开启";
         }
@@ -428,7 +431,10 @@ namespace ivf_tl_Operate.ViewModel
         public void CloseLed()
         {
             if (Serial == null) return;
-            Serial.CloseLedWait();
+            // M8-G3-1:LED 开关为串口设备命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "关闭LED灯",
+                () => Serial.CloseLedWait(),
+                input: new { houseSn = CurrentHouseId });
             //LedState = "关闭";
             LedState = KeyToStringConvert.GetLanguageStringByKey("C0305");
         }
@@ -439,7 +445,10 @@ namespace ivf_tl_Operate.ViewModel
         public void OpenIntake()
         {
             if (Serial == null) return;
-            Serial.OpenIntakeValveWait();
+            // M8-G3-1:进气阀开关为串口设备命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "打开进气阀",
+                () => Serial.OpenIntakeValveWait(),
+                input: new { houseSn = CurrentHouseId });
         }
 
         /// <summary>
@@ -448,7 +457,10 @@ namespace ivf_tl_Operate.ViewModel
         public void CloseIntake()
         {
             if (Serial == null) return;
-            Serial.CloseIntakeValveWait();
+            // M8-G3-1:进气阀开关为串口设备命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "关闭进气阀",
+                () => Serial.CloseIntakeValveWait(),
+                input: new { houseSn = CurrentHouseId });
         }
 
         /// <summary>
@@ -457,7 +469,10 @@ namespace ivf_tl_Operate.ViewModel
         public void OpenExhaust()
         {
             if (Serial == null) return;
-            Serial.OpenExhaustValveWait();
+            // M8-G3-1:排气阀开关为串口设备命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "打开排气阀",
+                () => Serial.OpenExhaustValveWait(),
+                input: new { houseSn = CurrentHouseId });
         }
 
 
@@ -467,7 +482,10 @@ namespace ivf_tl_Operate.ViewModel
         public void CloseExhaust()
         {
             if (Serial == null) return;
-            Serial.CloseExhaustValveWait();
+            // M8-G3-1:排气阀开关为串口设备命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "关闭排气阀",
+                () => Serial.CloseExhaustValveWait(),
+                input: new { houseSn = CurrentHouseId });
         }
 
         /// <summary>
@@ -476,7 +494,10 @@ namespace ivf_tl_Operate.ViewModel
         public void HouseAeration()
         {
             if (Serial == null) return;
-            Serial.HouseAerationWait();
+            // M8-G3-1:舱室补气为串口设备命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "舱室补气",
+                () => Serial.HouseAerationWait(),
+                input: new { houseSn = CurrentHouseId });
         }
 
         /// <summary>
@@ -485,7 +506,10 @@ namespace ivf_tl_Operate.ViewModel
         public void HouseVent()
         {
             if (Serial == null) return;
-            Serial.HouseVentWait();
+            // M8-G3-1:舱室排气为串口设备命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "舱室排气",
+                () => Serial.HouseVentWait(),
+                input: new { houseSn = CurrentHouseId });
         }
 
 
@@ -497,7 +521,10 @@ namespace ivf_tl_Operate.ViewModel
         {
             if (Serial == null) return;
             // M1-B2:走 lease.Serial→control ComBin(builder 与 operate 逐字节一致)。⚠ 待真机 V-010(写E方回包判定)。
-            Serial.WriteOpenIntakeTimeWait(newValue, isBuffer: false);
+            // M8-G3-1:写舱室进气阀时间为写 EEPROM 设备命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "写舱室进气阀时间",
+                () => Serial.WriteOpenIntakeTimeWait(newValue, isBuffer: false),
+                input: new { houseSn = CurrentHouseId, newValue });
         }
 
         /// <summary>
@@ -509,7 +536,11 @@ namespace ivf_tl_Operate.ViewModel
             if (Serial == null) return;
             // ⚠ M1-B2:control Commander 缺 CreateWriteEEPROOpenVentTimeCommand(T1.1 核实),不臆造字节 → 暂不下发。
             // 待真机核对字节后在 control Commander 补 builder 再接入。当前仅本地回显,未真正写入下位机。
-            if (!Serial.WriteOpenVentTimeWait(newValue))
+            // M8-G3-1:写舱室排气阀时间为写 EEPROM 设备命令入口,补操作日志埋点(module=对焦调试)。
+            var written = Aivfo.OperationLog.OperationLogger.Run("对焦调试", "写舱室排气阀时间",
+                () => Serial.WriteOpenVentTimeWait(newValue),
+                input: new { houseSn = CurrentHouseId, newValue });
+            if (!written)
             {
                 AddMessageInfo($"[排气阀时间]control 端暂缺写E方命令(待真机补字节),本次未下发到下位机,仅本地暂存{newValue}");
             }
@@ -535,7 +566,10 @@ namespace ivf_tl_Operate.ViewModel
         {
             if (Serial == null) return;
             // M1-B2:走 lease.Serial→control ComBin(builder 与 operate 逐字节一致)。⚠ 待真机 V-010。
-            Serial.WriteScanStepWait(newValue);
+            // M8-G3-1:写垂直电机间隔脉冲为写 EEPROM 设备命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "写垂直电机间隔脉冲",
+                () => Serial.WriteScanStepWait(newValue),
+                input: new { houseSn = CurrentHouseId, newValue });
         }
 
         /// <summary>
@@ -573,7 +607,10 @@ namespace ivf_tl_Operate.ViewModel
         public void HorizontalMotorForward(int value)
         {
             if (Serial == null) return;
-            Serial.HorizontalForwardWait(value, tLSetting.motorDelay);
+            // M8-G3-1:电机控制命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "水平电机正向",
+                () => Serial.HorizontalForwardWait(value, tLSetting.motorDelay),
+                input: new { houseSn = CurrentHouseId, value });
             CurrentHor += value;
         }
 
@@ -583,7 +620,10 @@ namespace ivf_tl_Operate.ViewModel
         public void HorizontalMotorBackward(int value)
         {
             if (Serial == null) return;
-            Serial.HorizontalBackwardWait(value, tLSetting.motorDelay);
+            // M8-G3-1:电机控制命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "水平电机反向",
+                () => Serial.HorizontalBackwardWait(value, tLSetting.motorDelay),
+                input: new { houseSn = CurrentHouseId, value });
             CurrentHor -= value;
         }
 
@@ -600,7 +640,10 @@ namespace ivf_tl_Operate.ViewModel
                 AddMessageInfo($"[未获取到{newWell}号well的水平电机位置]");
                 return;
             }
-            Serial.HorizontalMoveToWait(currentHorSetting.horizontalMotorPosition, tLSetting.motorDelay);
+            // M8-G3-1:电机控制命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "水平电机到well",
+                () => Serial.HorizontalMoveToWait(currentHorSetting.horizontalMotorPosition, tLSetting.motorDelay),
+                input: new { houseSn = CurrentHouseId, well = newWell, hor = currentHorSetting.horizontalMotorPosition });
             CurrentHor = currentHorSetting.horizontalMotorPosition;
             CurrentWell = newWell;
         }
@@ -624,7 +667,10 @@ namespace ivf_tl_Operate.ViewModel
         public void VerticalMotorForward(int newValue)
         {
             if (Serial == null) return;
-            Serial.VerticalForwardWait(newValue, tLSetting.motorDelay);
+            // M8-G3-1:电机控制命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "垂直电机正向",
+                () => Serial.VerticalForwardWait(newValue, tLSetting.motorDelay),
+                input: new { houseSn = CurrentHouseId, newValue });
             CurrentVer += newValue;
         }
 
@@ -634,7 +680,10 @@ namespace ivf_tl_Operate.ViewModel
         public void VerticalMotorBackward(int newValue)
         {
             if (Serial == null) return;
-            Serial.VerticalBackwardWait(newValue, tLSetting.motorDelay);
+            // M8-G3-1:电机控制命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "垂直电机反向",
+                () => Serial.VerticalBackwardWait(newValue, tLSetting.motorDelay),
+                input: new { houseSn = CurrentHouseId, newValue });
             CurrentVer -= newValue;
         }
 
@@ -644,7 +693,10 @@ namespace ivf_tl_Operate.ViewModel
         public void VerticalMotorAbsolute(int newValue)
         {
             if (Serial == null) return;
-            Serial.VerticalMoveToWait(newValue, tLSetting.motorDelay);
+            // M8-G3-1:电机控制命令入口,补操作日志埋点(module=对焦调试)。
+            Aivfo.OperationLog.OperationLogger.Run("对焦调试", "垂直电机绝对运动",
+                () => Serial.VerticalMoveToWait(newValue, tLSetting.motorDelay),
+                input: new { houseSn = CurrentHouseId, newValue });
             CurrentVer = newValue;
         }
 
@@ -668,6 +720,11 @@ namespace ivf_tl_Operate.ViewModel
                 verNewValue = cc.clearestPosition;
             }
 
+            // M8-G3-1:一键电机准备为多步串口动作流程,用 Begin scope 串联各子动作(module=对焦调试)。
+            using var _op = Aivfo.OperationLog.OperationLogger.Begin("对焦调试", "一键电机准备",
+                houseSn: CurrentHouseId);
+            try { _op.Input(new { houseSn = CurrentHouseId, targetHor = currentHorSetting.horizontalMotorPosition, targetVer = verNewValue }); } catch { }
+
             Serial.HorizontalResetWait(tLSetting.motorDelay);
             CurrentHor = 0;
             CurrentWell = 0;
@@ -695,7 +752,11 @@ namespace ivf_tl_Operate.ViewModel
                 if (GetPicData() == 0)
                 {
                     if (Cam == null) return false;
-                    return Cam.SavePic(fullName, CurrentHouse.ccdWidth, CurrentHouse.ccdHeight);
+                    // M8-G3-1:单张抓拍落盘为相机采图+保存命令入口,补操作日志埋点(module=对焦调试)。
+                    // 批量抓图(水平抓图/清晰图层抓图)经 Begin scope 调用时,本埋点自动成为其子埋点(父子链)。
+                    return Aivfo.OperationLog.OperationLogger.Run("对焦调试", "单张抓拍",
+                        () => Cam.SavePic(fullName, CurrentHouse.ccdWidth, CurrentHouse.ccdHeight),
+                        input: new { houseSn = CurrentHouseId, well = CurrentWell, focal = CurrentFocal, hor = CurrentHor, ver = CurrentVer });
 
                 }
                 return false;
@@ -718,6 +779,10 @@ namespace ivf_tl_Operate.ViewModel
                 IsStop = false;
                 if (Serial == null) return;
                 if (IsStop) return;
+                // M8-G3-1:水平抓图为多步循环采图流程,用 Begin scope 串联各 well 抓拍子埋点(module=对焦调试)。
+                using var _op = Aivfo.OperationLog.OperationLogger.Begin("对焦调试", "水平抓图",
+                    houseSn: CurrentHouseId);
+                try { _op.Input(new { houseSn = CurrentHouseId }); } catch { }
                 Serial.HorizontalResetWait(tLSetting.motorDelay);
                 CurrentHor = 0;
                 CurrentWell = 0;
@@ -781,6 +846,10 @@ namespace ivf_tl_Operate.ViewModel
                         AddMessageInfo($"[未获取到{CurrentWell}号well的自动对焦起点]");
                         return;
                     }
+                    // M8-G3-1:清晰图层抓图为多步循环采图流程,用 Begin scope 串联各层抓拍子埋点(module=对焦调试)。
+                    using var _op = Aivfo.OperationLog.OperationLogger.Begin("对焦调试", "清晰图层抓图",
+                        houseSn: CurrentHouseId);
+                    try { _op.Input(new { houseSn = CurrentHouseId, well = CurrentWell, focalCount, xun }); } catch { }
                     startAutoFocus = cc.autoFocusPosition;
                     string dtnow = string.Format("{0:yyyy-MM-dd-HH-mm-ss}", DateTime.Now); //24小时制
                     string path = null;
@@ -872,7 +941,11 @@ namespace ivf_tl_Operate.ViewModel
         public void SetExposure(int newExporuse)
         {
             if (Cam == null) { AddMessageInfo("设置曝光失败:相机句柄为空"); return; }
-            AddMessageInfo($"设置曝光结果:{Cam.SetExposure(newExporuse)}");
+            // M8-G3-1:设置相机曝光为相机设备命令入口,补操作日志埋点(module=对焦调试)。
+            var ret = Aivfo.OperationLog.OperationLogger.Run("对焦调试", "设置相机曝光值",
+                () => Cam.SetExposure(newExporuse),
+                input: new { houseSn = CurrentHouseId, ccdId = CurrentCCDId, newExporuse });
+            AddMessageInfo($"设置曝光结果:{ret}");
         }
 
         /// <summary>
@@ -1441,11 +1514,17 @@ namespace ivf_tl_Operate.ViewModel
                     return false;
                 }
 
+                // M8-G3-1:保存手调拍摄层为"本地校验/计算 + HTTP 持久化 + 内存同步"打包的关键业务操作,
+                // 用 Begin scope 给一个父操作名(统一 traceId 串联其内的 WellUpdateApi HTTP 埋点;module=对焦调试)。
+                using var _op = Aivfo.OperationLog.OperationLogger.Begin("对焦调试", "保存手调拍摄层设置",
+                    houseSn: CurrentHouseId);
+
                 // 校验:层数≥1、间距>0、下移≥0 且 <层数;非法不保存。
                 var eff = ResolveEffectiveTune(out string msg);
                 if (eff == null)
                 {
                     AddMessageInfo($"[手调拍摄层]保存被拦截(非法值):{msg}");
+                    try { _op.Output(new { rejected = msg }).Fail("校验未通过:" + msg); } catch { }
                     return false;
                 }
 
@@ -1454,6 +1533,7 @@ namespace ivf_tl_Operate.ViewModel
                 int? wellSpacing = string.IsNullOrWhiteSpace(ManualLayerSpacing) ? (int?)null : int.Parse(ManualLayerSpacing.Trim());
                 // 下移复用 move_down_layer(库列非空);留空时回退取生效值(eff.down)写回,不破坏旧列语义。
                 int wellMoveDown = string.IsNullOrWhiteSpace(ManualMoveDown) ? eff.Value.down : int.Parse(ManualMoveDown.Trim());
+                try { _op.Input(new { houseSn = CurrentHouseId, well = ManualTuneWell, wellCount, wellSpacing, wellMoveDown }); } catch { }
 
                 // 复用现有 well 设置保存通道:与 SetWellMotorPosition/SetWellAutoFocus 同一 WellUpdateApi。
                 // 后端按 tlSn/houseSn/wellSn 定位 house_well_setting 行做部分更新(仅本次携带的字段)。
@@ -1470,6 +1550,7 @@ namespace ivf_tl_Operate.ViewModel
                 if (!ok)
                 {
                     AddMessageInfo("[手调拍摄层]保存失败(接口返回失败)");
+                    try { _op.Fail("WellUpdateApi 返回失败"); } catch { }
                     return false;
                 }
 
@@ -1483,6 +1564,7 @@ namespace ivf_tl_Operate.ViewModel
                 }
                 AddMessageInfo($"[手调拍摄层]well{ManualTuneWell} 已存为默认:层数={(wellCount?.ToString() ?? "继承")} " +
                                $"间距={(wellSpacing?.ToString() ?? "继承")} 下移={wellMoveDown}(写 house_well_setting well 级覆盖,留空继承设备级)");
+                try { _op.Output(new { ok = true }).Success(); } catch { }
                 return true;
             }
             catch (Exception ex)

+ 44 - 36
ivf_tl_operate_2.0/ivf_tl_Operate/ViewModel/MainPageViewModel.cs

@@ -346,43 +346,47 @@ namespace ivf_tl_Operate.ViewModel
         /// <param name="houseSn"></param>
         public void EndDish(int houseSn)
         {
-            switch (houseSn)
+            // M8-G3-1:结束培养——本地清空该舱培养记录与发育/开始时间,属有业务含义的本地操作,埋点(自动计时+异常→失败)。
+            Aivfo.OperationLog.OperationLogger.Run("舱室操作", "结束培养", () =>
             {
-                case 1:
-                    ExDish1 = new ExDish();
-                    break;
-                case 2:
-                    ExDish2 = new ExDish();
-                    break;
-                case 3:
-                    ExDish3 = new ExDish();
-                    break;
-                case 4:
-                    ExDish4 = new ExDish();
-                    break;
-                case 5:
-                    ExDish5 = new ExDish();
-                    break;
-                case 6:
-                    ExDish6 = new ExDish();
-                    break;
-                case 7:
-                    ExDish7 = new ExDish();
-                    break;
-                case 8:
-                    ExDish8 = new ExDish();
-                    break;
-                case 9:
-                    ExDish9 = new ExDish();
-                    break;
-                case 10:
-                    ExDish10 = new ExDish();
-                    break;
-            }
-            var exhoue = HouseSnToExHouse(houseSn);
-            if (exhoue == null) return;
-            exhoue.FaYuTime = "";
-            exhoue.StartTime = "";
+                switch (houseSn)
+                {
+                    case 1:
+                        ExDish1 = new ExDish();
+                        break;
+                    case 2:
+                        ExDish2 = new ExDish();
+                        break;
+                    case 3:
+                        ExDish3 = new ExDish();
+                        break;
+                    case 4:
+                        ExDish4 = new ExDish();
+                        break;
+                    case 5:
+                        ExDish5 = new ExDish();
+                        break;
+                    case 6:
+                        ExDish6 = new ExDish();
+                        break;
+                    case 7:
+                        ExDish7 = new ExDish();
+                        break;
+                    case 8:
+                        ExDish8 = new ExDish();
+                        break;
+                    case 9:
+                        ExDish9 = new ExDish();
+                        break;
+                    case 10:
+                        ExDish10 = new ExDish();
+                        break;
+                }
+                var exhoue = HouseSnToExHouse(houseSn);
+                if (exhoue == null) return;
+                exhoue.FaYuTime = "";
+                exhoue.StartTime = "";
+            }, input: new { houseSn });
         }
 
         public ExDish HouseSnToDish(int houseSn)
@@ -959,9 +963,13 @@ namespace ivf_tl_Operate.ViewModel
                     var aa = AlarmQueue.FirstOrDefault(x => x.id == id);
                     if (aa != null) aa.mute = mute;
                 }
+                // M8-G3-1:报警静音/取消静音——本地修改报警队列状态,属有业务含义的本地操作。原方法吞异常,故用兜底 Log 直记成功(不改控制流)。
+                Aivfo.OperationLog.OperationLogger.Log("报警操作", "设置报警静音", input: new { id, mute }, result: "成功");
             }
             catch (Exception ex)
             {
+                // M8-G3-1:失败兜底直记(沿用原异常吞处理,不重抛)。
+                Aivfo.OperationLog.OperationLogger.Log("报警操作", "设置报警静音", input: new { id, mute }, result: "失败", error: ex.GetType().Name + ": " + ex.Message);
                 AppData.Instance.LogHelper.ExceptionLog(ex, "SetAlarmMute", LogEnum.RunException);
             }
         }

+ 17 - 13
ivf_tl_operate_2.0/ivf_tl_Operate/ViewModel/UnifiedConfigViewModel.cs

@@ -60,21 +60,25 @@ namespace ivf_tl_Operate.ViewModel
         /// <summary>把界面编辑值写回 App.config(凭据加密)。返回是否成功(异常已在 Helper 内吞掉,恒 true 占位)。</summary>
         public bool SaveAll()
         {
-            AppConfigHelper.Save("urlIp", UrlIp);
-            AppConfigHelper.Save("urlPort", UrlPort);
-            AppConfigHelper.Save("mqttIp", MqttIp);
-            AppConfigHelper.Save("mqttPort", MqttPort);
-            AppConfigHelper.Save("kfkaIP", KfkaIP);
-            AppConfigHelper.Save("kfkaPort", KfkaPort);
+            // M8-G3-1:保存统一配置=写 App.config 并加密落盘凭据,属真实业务写操作,需记操作日志(凭据不入 input)。
+            Aivfo.OperationLog.OperationLogger.Run("统一配置", "保存统一配置", () =>
+            {
+                AppConfigHelper.Save("urlIp", UrlIp);
+                AppConfigHelper.Save("urlPort", UrlPort);
+                AppConfigHelper.Save("mqttIp", MqttIp);
+                AppConfigHelper.Save("mqttPort", MqttPort);
+                AppConfigHelper.Save("kfkaIP", KfkaIP);
+                AppConfigHelper.Save("kfkaPort", KfkaPort);
 
-            AppConfigHelper.Save("cacheDisk", CacheDisk);
-            AppConfigHelper.Save("tlNum", TlNum);
-            AppConfigHelper.Save("Language", Language);
-            AppConfigHelper.Save("houseEnabled", HouseEnabled);
+                AppConfigHelper.Save("cacheDisk", CacheDisk);
+                AppConfigHelper.Save("tlNum", TlNum);
+                AppConfigHelper.Save("Language", Language);
+                AppConfigHelper.Save("houseEnabled", HouseEnabled);
 
-            AppConfigHelper.Save("userName", UserName);
-            AppConfigHelper.SaveEncrypted("passWord", PassWord);
-            AppConfigHelper.SaveEncrypted(AppConfigHelper.EngineerPwdKey, EngineerPwd);
+                AppConfigHelper.Save("userName", UserName);
+                AppConfigHelper.SaveEncrypted("passWord", PassWord);
+                AppConfigHelper.SaveEncrypted(AppConfigHelper.EngineerPwdKey, EngineerPwd);
+            }, input: new { UrlIp, UrlPort, MqttIp, MqttPort, KfkaIP, KfkaPort, CacheDisk, TlNum, Language, HouseEnabled, UserName });
             return true;
         }
     }

+ 1 - 1
项目文档/开发计划/2026-06-20-当前开发计划.md

@@ -66,7 +66,7 @@
 
 **涉及文件**:operate 各 ViewModel/命令、Java `tl-control`/`business` 关键 Controller/Service(`@OperateLog`)、M8 §10 配置中心下发 + 本地兜底补送逻辑。
 
-- [ ] **G3-1** `[纯代码][P1]` operate 逐方法埋点(逐 ViewModel/命令接入)。
+- [x] **G3-1** `[纯代码][P1]` operate 逐方法埋点(逐 ViewModel/命令接入)。 ✅2026-06-20 完成:VM 层(HouseDebugPageVM ~23 法:LED/进排气阀/补排气/写EEPROM/水平垂直电机各向/抓图/一键电机准备/手调保存等、BufferDebugVM 3、MainPageVM 2:结束培养/报警静音、UnifiedConfigVM 1:保存配置排除密码) + View code-behind 层(加皿窗口:编辑保存/启动培养皿/启动平衡皿、胚胎详情:移动/释放/删除/作废/结束 5 个、舱室设置:保存舱室/保存系统设置、对焦设置:保存/批量校验)。事件处理器用 `OperationLogger.Begin` scope + 失败路径显式 `Fail()`(Dispose 默认记成功),不改控制流;HTTP/串口/相机已由 P3b 单点收口覆盖;展示类/纯数据 VM(Photo/Detail-VM/DishRecord/Alarm/Chart/ServiceMonitor)无操作正确跳过。operate 工程 `dotnet build` 0 error。★真入 operation_log 待起 Kafka+oplog 端到端抽查(运行期,非真机门控)。
 - [x] **G3-2** `[纯代码][P1]` Java 埋点:tl-control / business 关键 Controller/Service 加 `@OperateLog`。 ✅2026-06-20 完成:两服务装 M8 三件套(log-starter+kafka-starter+oplog-client)+ application-local 配 kafka(${server.ip}:9092);tl-control 15 Controller/90 方法、business 24 Controller/121 方法贴 `@OperateLog`(module=@Api tags、operation=@ApiOperation 中文值);两服务 Maven 编译 BUILD SUCCESS。★真入 operation_log 待起 Kafka+oplog 端到端抽查(运行期,非真机门控)。
 - [ ] **G3-3** `[纯代码][P2]` M8 配置集中下发 + 本地兜底补送(C4):改配置热生效;Kafka 恢复后补送。
 

+ 13 - 0
项目文档/进度/交接卡.md

@@ -514,3 +514,16 @@
 - 真机/运行待验:★真入 operation_log 表需起 Kafka+oplog 微服务端到端抽查(运行期,非真机硬件门控)——GUI/接口触发任一操作后查 log 库 operation_log 有记录即闭环★。
 - 下一步:G3 余下 G3-1(operate 逐方法埋点,C# 大量 ViewModel)/G3-3(配置下发 C4);或 G2 UI / G4 纯代码。
 - 提醒:本批改动含 .flattened-pom.xml(flatten 插件产物)与 target/classes 副本随编译变化,提交时留意是否纳入。
+
+## 2026-06-20 · G3-1 operate 逐方法埋点完成(C# 端 M8 操作日志铺开,10 subagent + 编译验证)
+- 背景:G3-2 Java 埋点后接续同域 G3-1。C# 机制(Aivfo.OperationLog 组件:Run/Begin/Log + OperationLogContext + 本地兜底)P3a/P3b 已就绪,P3b 已封装关键边界(HTTP 单点收口 HttpHelper、串口 OpenPort/ClosePort、相机 Init/UnInit、HouseDebugPageVM 几个命令)。G3-1 = 在其余 VM/命令逐方法手动埋点达成"操作全埋"。
+- 重大结构发现:operate MVVM 不规范,**很多真实用户操作写在 View 的 *.xaml.cs code-behind 的 _Click 事件里,不在 ViewModel**(VM 多为纯数据/展示)。故 G3-1 分两层铺:VM 层 + View code-behind 层。
+- 埋点方式(两类):①VM 内同步动作用 `OperationLogger.Run(模块,操作,()=>核心调用,input:...)`(自动计时+异常重抛,保持原异常流);多步流程用 `Begin` scope 串 traceId。②View code-behind 事件多自带 try/catch 吞异常不重抛,用 `using var _op = OperationLogger.Begin(模块,操作)` + try 末尾 `_op.Success()`/catch 开头 `_op.Fail(...)`。**铁律:OperationScope.Dispose 无法探测异常默认记"成功",所有失败路径(校验早return/接口false/catch)必须显式 Fail(),否则失败误记成功**。守卫子句留 scope 外,input 排除密码等敏感字段,全限定名不加 using,每处加 `// M8-G3-1:` 注释。
+- 已埋(10 个 subagent 分文件并行,互不触碰同一文件):
+  · VM 层:HouseDebugPageViewModel ~23 法(LED/进排气阀/补排气/写EEPROM×3/水平电机正反向/到well/垂直电机正反向/绝对/一键电机准备Begin/设曝光/抓拍/水平抓图Begin/清晰图层抓图Begin/保存手调拍摄层Begin)、BufferDebugViewModel 3(补气/写进气阀时间Begin/写灯光亮度Begin)、MainPageViewModel 2(EndDish 结束培养 Run、SetAlarmMute 报警静音改 Log 保留吞异常)、UnifiedConfigViewModel 1(SaveAll 保存统一配置,input 排除 PassWord/EngineerPwd)。
+  · View code-behind 层:AddDishWindowView(EditDishSave 保存皿/StartDish 启动培养皿/StartBalance 启停平衡皿)、DetailPageView(EmbryoMove/Free/Del/Inv/Over 5 个胚胎操作)、HouseSettingPageView(SaveHouse 保存舱室/SaveSetting 保存系统设置含8校验)、AutoFocusSettingView(SaveHouse 保存对焦设置/SettCheck 批量校验)。
+  · 正确跳过(说明在位):展示类/纯数据 VM(PhotoPageVM/DetailPageVM/DishRecordVM/AlarmPageVM/ChartPageVM/ServiceMonitorVM 纯展示无操作)、HouseDebugPageView 等 code-behind 仅转调已埋 VM 方法(不重复埋)、HTTP-only 方法(P3b 单点收口已覆盖)、导航/构造/UI 刷新/私有辅助。
+- ★编译核实:`dotnet build ivf_tl_Operate/ivf_tl_Operate.csproj -c Debug` **0 错误,已成功生成**(两轮:VM 层后一次、code-behind 层后一次)。BufferDebug 几条 MVVMTK0034(直接引用 currentHouse 字段)系原有代码非本次引入。codegraph sync up to date。
+- 抽查:AutoFocusSettingView.SaveHouse(守卫外置/Success 成功分支/4处 Fail 覆盖校验+接口false+catch+throw)、DetailPageView.EmbryoOver(守卫保留原try内/内层try Success+Fail+throw到外层原ExLog吞异常)——Success/Fail 放置正确、控制流逐字保留。
+- 真机/运行待验:★同 G3-2,真入 operation_log 待起 Kafka+oplog 端到端抽查(运行期,非真机硬件门控)——GUI 触发任一操作后查 log 库有记录即闭环★。
+- 下一步:G3 仅剩 G3-3(M8 §10 配置集中下发+本地兜底补送 C4);或 G2 UI(首页弹框/13子页自适应/TabTip/well三态)、G4-1(C6 对焦清理任务)纯代码任务。真机门控 G1-1/G4-3/G5 待用户在场。

+ 4 - 4
项目文档/进度/工作计划表.md

@@ -19,7 +19,7 @@
 
 **续接三件套(无缝接续,不依赖对话记忆)**:① 本表(里程碑状态)② 《2026-06-20-当前开发计划.md》(剩余工作)③ 进度状态.yaml(当前断点)。
 
-**剩余工作总览**(详见《当前开发计划》):G1 串口收尾(T1.4 真机/ComBin 枚举迁移去重/写EEPROM V-010)|G2 UI(首页舱室弹框位置大小/13 子页自适应/TabTip/well 三态)|G3 日志铺开(operate 逐方法/**Java 埋点 ✅2026-06-20 完成**/C4 配置下发)|G4 对焦·数据(C6 清理任务/**C2 建库 runner ✅2026-06-20 完成**/V-046 列迁移)|G5 业务回归(M6/M7)。多数受真机 GUI 门控。
+**剩余工作总览**(详见《当前开发计划》):G1 串口收尾(T1.4 真机/ComBin 枚举迁移去重/写EEPROM V-010)|G2 UI(首页舱室弹框位置大小/13 子页自适应/TabTip/well 三态)|G3 日志铺开(**operate 逐方法 ✅2026-06-20**/**Java 埋点 ✅2026-06-20**/C4 配置下发)|G4 对焦·数据(C6 清理任务/**C2 建库 runner ✅2026-06-20 完成**/V-046 列迁移)|G5 业务回归(M6/M7)。多数受真机 GUI 门控。
 
 ---
 
@@ -148,10 +148,10 @@
 | M8-02 | Kafka topic tl-oplog + 统一 schema | ☑ | M8-01 | 两端日志格式一致 | 14 §5/6;三端同 schema | ✔ |
 | M8-03 | Java @OperateLog 切面机制 + 关键方法注解 | 🟢 | M8-02 | Java 操作入库、trace 串联 | 14 §8;机制✔ + data-transmission✔;**tl-control 15C/90法 + business 24C/121法 已贴 @OperateLog + 装三件套(G3-2),编译 BUILD SUCCESS;真入库待 Kafka+oplog 运行验** | ✔ |
 | M8-04 | C# 组件 Aivfo.OperationLog | ☑ | M8-02 | 组件可复用 | 14 §9;P3a 端到端✔ | ✔ |
-| M8-05 | C# 全埋 operate + front | 🔶 | M8-04 | 操作全覆盖 | 14 §2/9;operate 边界埋点✔ + **front 接入✔(C5)**;**operate 逐方法手埋 待** | ✔ |
+| M8-05 | C# 全埋 operate + front | 🟢 | M8-04 | 操作全覆盖 | 14 §2/9;operate 边界埋点✔ + **front 接入✔(C5)** + **operate 逐方法手埋✔(G3-1:VM 层 HouseDebug/Buffer/Main/UnifiedConfig + View code-behind 加皿/胚胎/舱室设置/对焦设置命令入口,0 error)**;真入库待运行验 | ✔ |
 | M8-06 | 可配置 + 配置集中下发 / 本地兜底补送 | 🔶 | M8-03,M8-05 | 配置热生效 | 14 §10;组件开关✔ + 规约✔;**§10 集中下发/补送 待(C4)** | ✔ |
 
-**M8 总评**:🔶 机制全 + front 接入本会话补齐;operate 逐方法/Java(tl-control/business) 埋点、配置下发(C4) 仍欠账
+**M8 总评**:🔶 机制全 + front 接入 + operate 逐方法埋点(G3-1) + Java(tl-control/business)埋点(G3-2) 均本会话补齐;仅配置集中下发(C4) 仍欠账,真入库待 Kafka+oplog 运行抽查
 
 ---
 
@@ -161,7 +161,7 @@
 
 - **G1 串口收尾**:T1.4 真机验证(借用→暂停→调试复用句柄→归还恢复,不报端口占用,需 GUI);ComBin 枚举阶段迁移去重(B 栈枚举阶段未迁移、未全删,迁移后真机验);写 EEPROM 调试动作补全(待 V-010 真机)。
 - **G2 UI**:首页舱室弹框位置/大小修复;M4 13 子页自适应(去写死像素 + Viewbox);TabTip 程序级屏蔽;well 三态色彩。
-- **G3 日志铺开**:operate 逐方法埋点;Java(tl-control/business)埋点;M8 §10 配置集中下发 / 本地兜底补送(C4)
+- **G3 日志铺开**:~~operate 逐方法埋点(G3-1✅2026-06-20)~~;~~Java(tl-control/business)埋点(G3-2✅)~~;M8 §10 配置集中下发 / 本地兜底补送(C4,剩 G3-3)
 - **G4 对焦/数据**:对焦标定清理任务(C6);建库脚本与 migration 合并 + 幂等 runner(C2);本地 SQLite 列迁移(V-046)。
 - **G5 业务回归 / 真机验收**:M6 业务回归;M7 真机验收(按待验证清单逐条,含 operate 完整 GUI 业务流程、对焦实拍、首页舱室弹框、M4 子页自适应视觉)。
 

+ 6 - 5
项目文档/进度/进度数据.js

@@ -1,10 +1,10 @@
 // 进度数据(监控面板.html 读取)。每完成一步由助手回写,generatedAt 用于停滞检测。
 window.PROGRESS_DATA = {
   project: "时差培养箱合并改造",
-  generatedAt: "2026-06-20T19:20:00",
-  currentTask: "★G3-2 Java 操作日志埋点完成(tl-control+business 接入 M8)★:两服务各装三件套(log-starter+kafka-starter+oplog-client)+application-local 配 kafka;tl-control 15 Controller/90方法、business 24 Controller/121方法贴 @OperateLog(module=@Api tags/operation=@ApiOperation 中文)。两服务 Maven 编译 BUILD SUCCESS。真入 operation_log 待起 Kafka+oplog 端到端抽查(运行期)。前序:G4-2 建库脚本合并(临时容器幂等验证通过)。",
-  currentStep: "G3-2 Java 埋点完成",
-  nextStep: "G3 余下:G3-1(operate 逐方法埋点,C# 大量 ViewModel)/G3-3(配置下发 C4);或 G2 UI(弹框/13子页自适应)/G4(C6清理任务)。真机门控 G1-1/G4-3/G5 待用户在场。",
+  generatedAt: "2026-06-20T22:10:00",
+  currentTask: "★G3-1 operate 逐方法埋点完成(M8 全量操作日志 C# 端铺开)★:VM 层(HouseDebugPageVM ~23法/BufferDebugVM 3/MainPageVM 2/UnifiedConfigVM 1) + View code-behind 层(加皿窗口编辑保存/启动培养皿/启动平衡皿、胚胎详情移动/释放/删除/作废/结束、舱室设置保存舱室/系统设置、对焦设置保存/批量校验)。事件处理器用 OperationLogger.Begin scope + 失败路径显式 Fail()(Dispose 默认记成功),不改控制流;HTTP/串口/相机已P3b单点收口覆盖;展示类/纯数据VM正确跳过。operate dotnet build 0 error。真入 operation_log 待起 Kafka+oplog 端到端抽查(运行期)。",
+  currentStep: "G3-1 operate 逐方法埋点完成",
+  nextStep: "G3 余下:G3-3(M8 §10 配置集中下发+本地兜底补送 C4);或 G2 UI(首页弹框/13子页自适应/TabTip/well三态)、G4(C6对焦清理任务)。真机门控 G1-1/G4-3/G5 待用户在场。",
   phase: "★★三项目合并物理收尾完成 + 串口占用代码层已修(T1.1~T1.3,T1.4待GUI验) + Phase0功能bug已修(scene=0不拍照/kfka重复键/front去imageScore) + 真机硬件+API+服务起全闭环★★ 文档重组进行中,真机GUI全流程验收待续",
   note: "2026-06-20:在2026-06-19灾后恢复基础上,本会话完成三项目合并最后一公里——control物理并入operate/control/(顶层ivf_tl_control_2.0消失,operate自包含)、autofocustool删除、编译operate/front/单测三关0错误。真机验证:硬件层(7舱握手+三路温度+压力+舱门+电机偏差0+相机出图2592×1944+CCDSN映射)+业务API(登录/tl-control/business/surface getButtons/对焦下发V-047上行V-064)全闭环;data-transmission补建aivfo-tl库后Started(nacos 6服务)。串口占用代码层修复T1.1~T1.3(T1.4真机待GUI);功能bug修复T0.1~T0.3。M2-02单测重建23断言全通过。审计报告与会话续接文档内容已三层归位后删除。详见交接卡。",
   planTasks: [
@@ -17,6 +17,7 @@ window.PROGRESS_DATA = {
     { id: "Phase4", name: "★容错读取接崩点(B6)+HTTP失联标记(C3)+down<count校验下沉+front接日志(C5)★", status: "☑" },
     { id: "G4-2", name: "★建库脚本与migration合并+幂等runner(C2):对焦表列/operation_log并入base+init-database.sh,临时容器全量+幂等验证★", status: "☑" },
     { id: "G3-2", name: "★Java操作日志埋点(tl-control 15C/90法+business 24C/121法贴@OperateLog)+两服务装M8三件套,编译BUILD SUCCESS;真入库待Kafka+oplog运行验★", status: "☑" },
+    { id: "G3-1", name: "★operate逐方法埋点(VM层HouseDebug/Buffer/Main/UnifiedConfig+View code-behind加皿/胚胎/舱室设置/对焦设置命令入口,Begin scope+失败显式Fail),dotnet build 0 error;真入库待运行验★", status: "☑" },
     { id: "Task1-4", name: "进度文件组(4文件)", status: "☑" },
     { id: "Task5", name: "M0-00 文档源码审核", status: "☑" },
     { id: "Task6-8", name: "自动对焦数据层 SQL 迁移", status: "☑" },
@@ -88,7 +89,7 @@ window.PROGRESS_DATA = {
       { id: "M8-02", name: "Kafka topic tl-oplog+三端同schema", status: "☑", env: false },
       { id: "M8-03", name: "Java @OperateLog切面(机制✔+data-trans✔+tl-control/business已接G3-2,真入库待运行验)", status: "🟢", env: false },
       { id: "M8-04", name: "C#组件 Aivfo.OperationLog(P3a端到端✔)", status: "☑", env: false },
-      { id: "M8-05", name: "C#全埋operate+front(边界埋点+front接入✔,逐方法手埋待)", status: "🔶", env: false },
+      { id: "M8-05", name: "C#全埋operate+front(边界埋点+front接入+operate逐方法手埋G3-1✔,真入库待运行验)", status: "🟢", env: false },
       { id: "M8-06", name: "可配置+配置集中下发(组件开关✔,§10下发待C4)", status: "🔶", env: false }
     ]}
   ],

+ 7 - 6
项目文档/进度/进度状态.yaml

@@ -1,13 +1,14 @@
 # 续接断点状态(机器可解析)。换会话/换电脑后首先读它定位。
 # 状态取值: 未开始 / 进行中 / 完成 / 代码完成待验证
 # 纪律:本字段只存【当前断点】,历史细节进 交接卡.md(见 CLAUDE.md 第三节)。
-更新时间: 2026-06-20 G3-2 完成(tl-control/business Java 操作日志埋点
+更新时间: 2026-06-20 G3-1 完成(operate 逐方法埋点,C# 端 M8 操作日志铺开
 当前任务: >
-  【★G3-2 Java 埋点完成(tl-control + business 接入 M8 操作日志)★】
-  · 两服务各装三件套:service+log-starter、manage+kafka-starter、lanucher+oplog-client;application-local 配 kafka(${server.ip}:9092, producer.enable=true)。
-  · tl-control 15 Controller/90 方法、business 24 Controller/121 方法贴 @OperateLog(module=@Api tags、operation=@ApiOperation 中文值);两服务 Maven 编译 BUILD SUCCESS。
-  · ★真入 operation_log 待起 Kafka+oplog 端到端抽查(运行期,非真机硬件门控)★。
-  下一步:G3-1(operate 逐方法埋点,C# 大量 ViewModel)或 G3-3(配置下发 C4);或挑 G2/G4 纯代码任务。真机门控 G1-1/G4-3/G5 待用户在场。
+  【★G3-1 operate 逐方法埋点完成(M8 全量操作日志 C# 端铺开)★】
+  · VM 层:HouseDebugPageVM ~23 法(LED/进排气阀/补排气/写EEPROM/水平垂直电机各向/抓图/一键电机准备/手调保存)、BufferDebugVM 3、MainPageVM 2(结束培养/报警静音)、UnifiedConfigVM 1(保存配置,排除密码)。
+  · View code-behind 层(真实操作所在):加皿窗口(编辑保存/启动培养皿/启动平衡皿)、胚胎详情(移动/释放/删除/作废/结束 5 个)、舱室设置(保存舱室/保存系统设置)、对焦设置(保存/批量校验)。
+  · 事件处理器用 OperationLogger.Begin scope + 失败路径显式 Fail()(Dispose 默认记成功),不改控制流;HTTP/串口/相机已 P3b 单点收口覆盖;展示类/纯数据 VM 正确跳过。operate dotnet build 0 error。
+  · ★真入 operation_log 待起 Kafka+oplog 端到端抽查(运行期,非真机门控)★。
+  下一步:G3-3(M8 §10 配置集中下发+本地兜底补送 C4,剩 G3 最后一项);或挑 G2 UI(首页弹框/13子页自适应/TabTip/well三态)/G4-1(C6 对焦清理任务)纯代码任务。真机门控 G1-1/G4-3/G5 待用户在场。
   续接读:《工作计划表》+《当前开发计划》+ 本文件 + 交接卡末尾。
 说明: >
   M0-M5 全部【可写源码】已完成,C#合并端 0 error + M2-02 单测 15/15。工具链就位(JDK11.0.25 + Maven3.9.9