// 编辑直写库:虚拟模式单元格提交后,按 rowid 直接 UPDATE 报价库(300ms 防抖聚合成单事务), // 同事务读回触发器算好的派生值与上级聚合行,回填内存读缓存并刷新显示。 // 数据库是唯一数据源:内存表只读,不再有任何"保存回写"路径。 using System; using System.Collections.Generic; using System.Data; using System.Windows.Forms; namespace 成套报价软件 { public partial class MainForm { /// 待写库项:表名|rowid|列 → 值(后写覆盖先写)。 private readonly Dictionary _待写项 = new Dictionary(); /// 防抖定时器(300ms):把窗口期内的编辑聚合成一个事务写库。 private Timer _直写Timer; /// 项目页同步防抖定时器(800ms):编辑报价三表后,把图号汇总刷进项目列表库 /// 并刷新项目页网格显示(箱柜台数/成本总价/报出总价/利润实时可见)。 private Timer _项目页同步Timer; /// 切项目页前等待报价页待写批完成:保证项目页读取的是已提交数据库值。 /// UI线程用 await 让 WinForms Timer/后台直写继续运行,不阻塞窗口。 private async System.Threading.Tasks.Task 等待直写完成() { for (int i = 0; i < 60; i++) // 最多 3 秒 { bool pending; lock (_待写项) pending = _待写项.Count > 0; if (!pending && !_直写进行中) return; await System.Threading.Tasks.Task.Delay(50); } Log("[项目页同步] 等待直写超过3秒,继续刷新(请检查直写日志)"); } /// 编辑报价三表后调用:防抖启动项目页同步(连续编辑只刷一次)。 private void 启动项目页同步() { if (_项目页同步Timer == null) { _项目页同步Timer = new Timer(); _项目页同步Timer.Interval = 800; _项目页同步Timer.Tick += (s, e) => { // ★ 直写队列未清空前不 SUM(编辑中批被防抖重置推迟,此刻汇总查询必读图号表旧值 // 写错项目页后不再纠正——实测 01:21 连续 4 轮绑定旧值 10069.5)。等队列清空再刷。 if (_直写进行中 || _待写项.Count > 0) { _项目页同步Timer.Stop(); _项目页同步Timer.Interval = 300; // 密集重试,直写批提交后立刻有机会刷 _项目页同步Timer.Start(); return; } _项目页同步Timer.Stop(); _项目页同步Timer.Interval = 800; // 恢复常规防抖间隔 try { if (string.IsNullOrWhiteSpace(_currentProjectId)) return; // 1) 图号表 SUM → 项目列表库(箱柜台数/成本总价/报出总价/利润/利润率) 计算引擎.刷新项目汇总(GetQuoteDbPath(_currentProjectId), _currentProjectId, TuhaoTableName); // 2) 刷新项目页网格显示 ReloadProjectsFromDatabase(); SetStatus("项目汇总已同步"); } catch (Exception exSync) { Log("[项目页同步] 异常: " + exSync.Message); } }; } _项目页同步Timer.Stop(); _项目页同步Timer.Start(); } /// 当前 Reload 耗时测量(进 Reload 方法时重启)。 private readonly System.Diagnostics.Stopwatch _swReload = System.Diagnostics.Stopwatch.StartNew(); /// 公式表缓存(管线建触发器时用;图号粘贴管线开头加载)。 private List<公式条目> _公式_元件, _公式_箱号, _公式_图号; /// 待建触发器清单 [yj表, 箱号表, 箱号ID, 图号ID](数据写完后统一建,避免事务内 DDL 拖慢批量 INSERT)。 private readonly System.Collections.Generic.List _待建yj触发器 = new System.Collections.Generic.List(); /// 待建箱号表触发器 [箱号表, 图号ID]。 private readonly System.Collections.Generic.List _待建xh触发器 = new System.Collections.Generic.List(); /// 进项目时清理历史版本遗留的触发器(触发器方案已退役,应用层集中刷新)。后台执行。 private void EnsureFormulaTriggersQuiet(string projectId) { System.Threading.Tasks.Task.Run(delegate { try { if (string.IsNullOrWhiteSpace(projectId)) return; string dbPath = GetQuoteDbPath(projectId); if (!System.IO.File.Exists(dbPath)) return; using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); 计算引擎.清理遗留触发器(conn); } } catch (Exception ex) { Log("[触发器清理] 失败: " + ex.Message); } }); } /// 保存按钮(功能区"保存"):实时架构下数据已在库,只刷跨库项目汇总并提示。 private void 保存按钮_实时保存提示() { try { 计算引擎.刷新项目汇总(GetQuoteDbPath(_currentProjectId), _currentProjectId, TuhaoTableName); } catch (Exception ex) { Log("[保存] 刷新项目汇总异常: " + ex.Message); } SetStatus("数据已实时保存在数据库"); MessageBox.Show(this, "数据已实时保存在数据库:\n编辑即写库,价格由数据库触发器自动计算,无需手动保存。", "保存", MessageBoxButtons.OK, MessageBoxIcon.Information); } /// /// ★ Ctrl+S 强制保存(2026-09-30):报价页/选型页通用快捷键。 /// ★ G4(2026-10-01)新增:Ctrl+N 新建项目 / Ctrl+O 打开项目表选中项目 / Ctrl+Z 撤销删除。 /// 拦截窗体级命令键(比 KeyDown 早,且能抢在菜单/表格默认行为之前)。 /// protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { if (keyData == (Keys.Control | Keys.S)) { try { 强制保存_快捷键(); } catch (Exception ex) { Log("[Ctrl+S] 保存异常: " + ex.Message); SetStatus("保存失败: " + ex.Message); MessageBox.Show(this, "保存失败:\n" + ex.Message, "强制保存", MessageBoxButtons.OK, MessageBoxIcon.Error); } return true; // 吃掉按键,不再往下传 } if (keyData == (Keys.Control | Keys.N)) // 新建项目 { try { button1_Click(null, null); } catch { } return true; } if (keyData == (Keys.Control | Keys.O)) // 打开项目表选中项目 { try { var row = GetFirstSelectedRow(this.advancedDataGridView6); if (row != null) { advancedDataGridView6_CellDoubleClick(this, new DataGridViewCellEventArgs(0, row.Index)); } else SetStatus("请先在项目表选中一个项目再按 Ctrl+O"); } catch (Exception exO) { Log("[Ctrl+O] 打开项目异常: " + exO.Message); } return true; } if (keyData == (Keys.Control | Keys.Z)) // 撤销最近一次删除 { try { MenuUndoDelete_Click(null, null); } catch (Exception exZ) { Log("[Ctrl+Z] 撤销异常: " + exZ.Message); } return true; } return base.ProcessCmdKey(ref msg, keyData); } /// /// Ctrl+S 强制保存链:① 结束所有表格编辑态(编辑中的值进直写队列) /// ② 直写队列同步落库 ③ 选型页未落库修改回写 ④ 刷报价三表+项目汇总 ⑤ 状态栏提示。 /// 与保存按钮不同:不弹 MessageBox,保存结果只在状态栏提示(不打断操作)。 /// private void 强制保存_快捷键() { // 未打开项目:没有可保存内容 if (string.IsNullOrWhiteSpace(_currentProjectId)) { SetStatus("未打开项目,无需保存"); return; } // (1) 结束编辑态——报价三表 + 选型页两表(编辑中的单元格才会提交/入队) if (this.advancedDataGridView9 != null) try { this.advancedDataGridView9.EndEdit(); } catch { } if (this.advancedDataGridView4 != null) try { this.advancedDataGridView4.EndEdit(); } catch { } if (this.advancedDataGridView5 != null) try { this.advancedDataGridView5.EndEdit(); } catch { } if (this.advancedDataGridView1 != null) try { this.advancedDataGridView1.EndEdit(); } catch { } if (this.advancedDataGridView12 != null) try { this.advancedDataGridView12.EndEdit(); } catch { } // (2) 直写队列强制落库(同步驱动,最多约 6 秒) 直写队列强制落库(); // (3) 选型页未落库的修改回写(箱号列编辑/替换产生的脏数据) try { if (_selectionSummaryDirty) SyncSelectionChangesToDb(); } catch (Exception exSel) { Log("[Ctrl+S] 选型页回写异常: " + exSel.Message); } // (4) 报价三表收尾 + 刷项目汇总(EndEdit 三表 + 计算引擎.刷新项目汇总) SaveAllToDatabaseSilent(); // (5) 状态栏反馈(保存状态标签转绿 + 提示区显示时间) 标记已保存(); SetStatus("已强制保存(Ctrl+S) " + DateTime.Now.ToString("HH:mm:ss")); } /// 单元格提交后调用:入队待写项(库中无 rowid 的行跳过——如尚未 Reload 的新插入行)。 private void 入队直写库(DataGridView dgv, int rowIndex, string colName, object value) { if (dgv == null || string.IsNullOrEmpty(colName) || rowIndex < 0) return; // ★ 项目页网格(advancedDataGridView6Table)不进直写队列:它在独立的项目列表库里, // 直写 Timer 用的是项目报价库连接——混进来会让整批 UPDATE 报错回滚, // 把同批正常的编辑(如报出系数)一起拖死(22:06 实测 SQL logic error 连刷)。 // 项目列表的汇总由 启动项目页同步/保存按钮 的 刷新项目汇总 负责。 if (dgv == this.advancedDataGridView6) return; var table = 取dgv数据表(dgv); if (table == null) return; // ★ TableName 为空的表(新建项目尚未 Reload 的内存图号表)不进直写—— // UPDATE "" SET ... 会 no such table,整批回滚把同批正常编辑拖死(00:03 实锤) // ★ 多选联动合并视图(2026-09-30):TableName 空 + 有"_来源表名_"列 = 合并表, // 按行级来源表名入队(每行来自不同的物理表) bool isMerged = string.IsNullOrEmpty(table.TableName) && table.Columns.Contains("_来源表名_"); if (string.IsNullOrEmpty(table.TableName) && !isMerged) return; if (!table.Columns.Contains("_rowid_")) return; if (rowIndex < 0 || rowIndex >= table.Rows.Count) return; long rowid = table.Rows[rowIndex]["_rowid_"] is long ? Convert.ToInt64(table.Rows[rowIndex]["_rowid_"]) : 0; if (rowid <= 0) return; // 行级来源表名(合并表)或整表表名(普通表) string rowTbl = isMerged ? Convert.ToString(table.Rows[rowIndex]["_来源表名_"]) : table.TableName; if (string.IsNullOrWhiteSpace(rowTbl)) return; // ★ A1(2026-09-30):统一定位体系——行有"内部ID"(三表都有)时直写按内部ID定位 // (rowid 只是兜底;移动行已不再重建表,rowid 也稳定,双保险) string rowNid = table.Columns.Contains("内部ID") ? Convert.ToString(table.Rows[rowIndex]["内部ID"]).Trim() : ""; lock (_待写项) { _待写项[rowTbl + "|" + rowid + "|" + colName] = new[] { rowTbl, rowid.ToString(), colName, value == null ? "" : value.ToString(), rowNid }; } // ★ 诊断(2026-09-30):合并视图编辑的实际写入目标(定位"改第一个箱号写到最后一箱号") if (isMerged) { try { string diagSrcXh = table.Columns.Contains("_来源图号_") ? Convert.ToString(table.Rows[rowIndex]["_来源图号_"]) : ""; Log("[合并直写诊断] dgv=" + dgv.Name + " 行=" + rowIndex + " 列=" + colName + " 值=" + (value == null ? "" : value.ToString()) + " → 表=" + rowTbl + " rowid=" + rowid + " 来源图号=" + diagSrcXh); } catch { } } 确保直写Timer().Stop(); 确保直写Timer().Start(); } private Timer 确保直写Timer() { if (_直写Timer == null) { _直写Timer = new Timer(); _直写Timer.Interval = 300; _直写Timer.Tick += 直写Timer_Tick; } return _直写Timer; } private void 直写Timer_Tick(object sender, EventArgs e) { 直写Timer_Tick_Async(); } /// 在途直写批的写库任务(仅"写库"部分,纯后台完成,不依赖 UI 消息——强制落库可安全 Wait 等它)。 private System.Threading.Tasks.Task _直写写库Task; /// 直写队列强制落库(关窗/保存/AI写库前调用):同步驱动直到队列清空。 /// ★ B1改造(2026-10-01):原实现 DoEvents+Sleep 轮询 120×50ms 泵消息等异步回调推进, /// 最坏卡界面 6 秒且依赖消息泵;现改为①等在途异步批的写库部分(纯后台 Task,Wait 不死锁) /// ②残留批在 UI 线程同步直写(批是防抖聚合后的少量 UPDATE,单事务毫秒级)。 /// 全程不泵消息、不依赖 Timer/续体,签名不变(6 处调用方零改动)。 public void 直写队列强制落库() { for (int i = 0; i < 200; i++) { List 批 = null; lock (_待写项) { if (_待写项.Count > 0 && !_直写进行中) { 批 = new List(_待写项.Values); _待写项.Clear(); } } if (批 != null) { string pid = _currentProjectId; if (string.IsNullOrWhiteSpace(pid)) return; string dbPath = GetQuoteDbPath(pid); if (!System.IO.File.Exists(dbPath)) return; var 受影响 = 执行写库批(dbPath, 批); if (受影响 != null) 直写批后处理(pid, dbPath, 受影响); continue; } bool pending; lock (_待写项) pending = _待写项.Count > 0; if (!pending && !_直写进行中) return; // 在途异步批:只等它的写库部分(后台完成,与 UI 续体无关,不会死锁) var t = _直写写库Task; if (_直写进行中 && t != null && !t.IsCompleted) { try { t.Wait(200); } catch { } } else System.Threading.Thread.Sleep(20); } Log("[直写库] 强制落库超时,队列仍有残留"); } private bool _直写进行中; // 重入闸:async void Tick 在后台写库期间再触发会两事务互踩(SQL logic error) private async void 直写Timer_Tick_Async() { _直写Timer.Stop(); if (_直写进行中) { 确保直写Timer().Start(); return; } // 上批未完:稍后重试(队列已被防抖聚合) if (_待写项.Count == 0) return; string pid = _currentProjectId; if (string.IsNullOrWhiteSpace(pid)) return; List 批; lock (_待写项) { 批 = new List(_待写项.Values); _待写项.Clear(); } string dbPath = GetQuoteDbPath(pid); if (!System.IO.File.Exists(dbPath)) return; _直写进行中 = true; // ★ B1改造(2026-10-01):写库段改为纯后台任务,完成时【在后台】清 _直写进行中—— // 强制落库只 Wait 写库任务(不依赖UI续体,不会死锁);读回/回填走UI续体,不拦截下一批。 var 写库完成 = new System.Threading.Tasks.TaskCompletionSource(); _直写写库Task = 写库完成.Task; // 表名→受影响 rowid(读回用) var 受影响 = new Dictionary>(); bool 写库失败 = false; // ★ 执行体Task赋局部变量:完成信号由 写库完成(TCS) 传递,执行体本身无人await—— // 赋值接收以消 CS4014(不await调用),fire-and-forget 语义不变 var 写库执行Task = System.Threading.Tasks.Task.Run(delegate { try { using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); using (var tx = conn.BeginTransaction()) { foreach (var item in 批) { string tbl = item[0], rowid = item[1], col = item[2], val = item[3]; string nid = item.Length > 4 ? item[4] : ""; // ★ A1(2026-09-30):直写 UPDATE 按内部ID定位(统一定位体系); // 行无内部ID(极老数据未迁移)时退回 rowid 兜底 string sqlUpd; if (!string.IsNullOrWhiteSpace(nid)) sqlUpd = "UPDATE \"" + tbl.Replace("\"", "") + "\" SET \"" + col.Replace("\"", "") + "\"=@v WHERE \"内部ID\"=@nid;"; else sqlUpd = "UPDATE \"" + tbl.Replace("\"", "") + "\" SET \"" + col.Replace("\"", "") + "\"=@v WHERE rowid=@r;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sqlUpd, conn, tx)) { cmd.Parameters.AddWithValue("@v", val); if (!string.IsNullOrWhiteSpace(nid)) cmd.Parameters.AddWithValue("@nid", nid); else cmd.Parameters.AddWithValue("@r", long.Parse(rowid)); cmd.ExecuteNonQuery(); } HashSet set; if (!受影响.TryGetValue(tbl, out set)) { set = new HashSet(); 受影响[tbl] = set; } set.Add(long.Parse(rowid)); } tx.Commit(); } } } catch (Exception exWrite) { // ★ 带批详情定位毒 SQL(表.列清单+完整异常),修完可留作排障 string 批详情 = ""; try { var parts = new List(); foreach (var i in 批) parts.Add(i[0] + "." + i[2] + "=" + (i[3] == null ? "" : i[3].ToString())); 批详情 = string.Join(" | ", parts.ToArray()); } catch { } Log("[直写库] 写库异常: " + exWrite.GetType().Name + " " + exWrite.Message + " | 批[" + 批.Count + "]: " + 批详情); 写库失败 = true; } finally { _直写进行中 = false; // ★ 后台清闸(B1):强制落库只等写库任务,不等UI续体 写库完成.TrySetResult(!写库失败); } }); await 写库完成.Task; if (写库失败) return; // ★ 批已提交落库:此刻 SUM 图号表才是新值——在此触发项目页同步, // 修复竞态(编辑入口的 800ms 同步可能跑在批提交前,SQL SUM 读旧总价写错汇总后不再纠正) 启动项目页同步(); // 读回触发器算好的行(本行+上级),回填缓存 try { var 读回 = new List(); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); foreach (var kv in 受影响) { string tbl = kv.Key; foreach (long rowid in kv.Value) 读回行(conn, tbl, rowid, 读回); // 上级行:元件表→箱号行+图号行;箱号表→图号行 var 上级 = 查上级(conn, pid, tbl, 读回); if (上级 != null) { foreach (var up in 上级) 读回行(conn, (string)up[0], long.Parse(up[1].ToString()), 读回); } } } if (读回.Count > 0) { var 快照 = 读回.ToArray(); BeginInvoke((MethodInvoker)delegate { try { 回填缓存(快照); } catch (Exception exUi) { Log("[直写库] 回填异常: " + exUi.Message); } }); } } catch (Exception exRead) { Log("[直写库] 读回异常: " + exRead.Message); } } /// 同步执行一批直写 UPDATE(单事务;强制落库路径用,UI线程直跑—— /// 批是防抖聚合后的少量 UPDATE,毫秒级)。返回 表名→受影响rowid 集;失败返回 null(内部已记日志)。 private Dictionary> 执行写库批(string dbPath, List 批) { var 受影响 = new Dictionary>(); try { using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); using (var tx = conn.BeginTransaction()) { foreach (var item in 批) { string tbl = item[0], rowid = item[1], col = item[2], val = item[3]; string nid = item.Length > 4 ? item[4] : ""; // 按内部ID定位(统一定位体系);行无内部ID(极老数据)退回 rowid 兜底 string sqlUpd; if (!string.IsNullOrWhiteSpace(nid)) sqlUpd = "UPDATE \"" + tbl.Replace("\"", "") + "\" SET \"" + col.Replace("\"", "") + "\"=@v WHERE \"内部ID\"=@nid;"; else sqlUpd = "UPDATE \"" + tbl.Replace("\"", "") + "\" SET \"" + col.Replace("\"", "") + "\"=@v WHERE rowid=@r;"; using (var cmd = new System.Data.SQLite.SQLiteCommand(sqlUpd, conn, tx)) { cmd.Parameters.AddWithValue("@v", val); if (!string.IsNullOrWhiteSpace(nid)) cmd.Parameters.AddWithValue("@nid", nid); else cmd.Parameters.AddWithValue("@r", long.Parse(rowid)); cmd.ExecuteNonQuery(); } HashSet set; if (!受影响.TryGetValue(tbl, out set)) { set = new HashSet(); 受影响[tbl] = set; } set.Add(long.Parse(rowid)); } tx.Commit(); } } return 受影响; } catch (Exception exWrite) { string 批详情 = ""; try { var parts = new List(); foreach (var i in 批) parts.Add(i[0] + "." + i[2] + "=" + (i[3] == null ? "" : i[3].ToString())); 批详情 = string.Join(" | ", parts.ToArray()); } catch { } Log("[直写库] 强制落库写库异常: " + exWrite.GetType().Name + " " + exWrite.Message + " | 批[" + 批.Count + "]: " + 批详情); return null; } } /// 直写批提交后的公共收尾:触发项目页汇总同步 + 读回派生/聚合行回填缓存。 private void 直写批后处理(string pid, string dbPath, Dictionary> 受影响) { // 批已提交落库,此刻 SUM 图号表才是新值 启动项目页同步(); try { var 读回 = new List(); using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnStringStatic(dbPath))) { conn.Open(); foreach (var kv in 受影响) { string tbl = kv.Key; foreach (long rowid in kv.Value) 读回行(conn, tbl, rowid, 读回); // 上级行:元件表→箱号行+图号行;箱号表→图号行 var 上级 = 查上级(conn, pid, tbl, 读回); if (上级 != null) { foreach (var up in 上级) 读回行(conn, (string)up[0], long.Parse(up[1].ToString()), 读回); } } } if (读回.Count > 0) { var 快照 = 读回.ToArray(); if (IsHandleCreated && !IsDisposed) BeginInvoke((MethodInvoker)delegate { try { 回填缓存(快照); } catch (Exception exUi) { Log("[直写库] 回填异常: " + exUi.Message); } }); } } catch (Exception exRead) { Log("[直写库] 读回异常: " + exRead.Message); } } /// 为库内全部数据表补"内部ID"索引(幂等,2026-10-01 B3): /// 直写 UPDATE / 箱号行聚合 / 重算定位大量 WHERE "内部ID" 查询,无索引则每次全表扫描。 /// 静态:两处调用点(项目库自检/方案库打开)有静态上下文。 private static void 确保数据表性能索引(System.Data.SQLite.SQLiteConnection conn) { try { var 表们 = new List(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 实际表名 IS NOT NULL AND 实际表名<>'';", conn)) using (var r = cmd.ExecuteReader()) while (r.Read()) 表们.Add(r.GetString(0)); foreach (var t in 表们) { string safe = t.Replace("\"", "").Replace("'", ""); if (safe.Length == 0) continue; // 老表可能还没有"内部ID"列——列存在才建索引 using (var chk = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM pragma_table_info('" + safe + "') WHERE name='内部ID';", conn)) if (Convert.ToInt64(chk.ExecuteScalar()) == 0) continue; using (var mk = new System.Data.SQLite.SQLiteCommand( "CREATE INDEX IF NOT EXISTS \"idx_" + safe + "_内部ID\" ON \"" + safe + "\"(\"内部ID\");", conn)) mk.ExecuteNonQuery(); } } catch (Exception ex) { OpenClawHost.写日志("[索引] 数据表内部ID索引(不影响使用): " + ex.Message); } } /// 查上级行:元件表→(箱号表行, 图号行);箱号表→图号行。返回 [表名,rowid] 列表。 private List 查上级(System.Data.SQLite.SQLiteConnection conn, string pid, string 表名, List 已读) { var result = new List(); string tuhaoTbl = TuhaoTableName; // 表索引查映射:元件表→(图号,箱号)→箱号表;箱号表→图号 string xhTbl = null, xhId = null, thId = null; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 类型,图号,箱号,实际表名 FROM 表索引 WHERE 实际表名=@t LIMIT 1;", conn)) { cmd.Parameters.AddWithValue("@t", 表名); using (var r = cmd.ExecuteReader()) { if (r.Read()) { string ty = r.IsDBNull(0) ? "" : r.GetString(0); thId = r.IsDBNull(1) ? "" : r.GetString(1); xhId = r.IsDBNull(2) ? "" : r.GetString(2); if (ty == "箱号") xhTbl = 表名; } } } if (xhTbl == null) { // 是元件表:找所属箱号表 if (string.IsNullOrEmpty(thId) || string.IsNullOrEmpty(xhId)) return result; using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 类型='箱号' AND 图号=@th LIMIT 1;", conn)) { cmd.Parameters.AddWithValue("@th", thId); object o = cmd.ExecuteScalar(); if (o != null && o != DBNull.Value) xhTbl = o.ToString(); } if (string.IsNullOrEmpty(xhTbl)) return result; 读回行ByID(conn, xhTbl, xhId, result); 读回行ByID(conn, tuhaoTbl, thId, result); } else { if (string.IsNullOrEmpty(thId)) return result; 读回行ByID(conn, tuhaoTbl, thId, result); } return result; } private void 读回行ByID(System.Data.SQLite.SQLiteConnection conn, string 表名, string 内部ID, List 出) { if (string.IsNullOrEmpty(内部ID)) return; var cols = new List(); long rowid = 0; var vals = new Dictionary(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT rowid, * FROM \"" + 表名.Replace("\"", "") + "\" WHERE \"内部ID\"=@id LIMIT 1;", conn)) { cmd.Parameters.AddWithValue("@id", 内部ID); using (var r = cmd.ExecuteReader()) { if (!r.Read()) return; rowid = Convert.ToInt64(r.GetValue(0)); for (int i = 1; i < r.FieldCount; i++) { cols.Add(r.GetName(i)); vals[r.GetName(i)] = r.IsDBNull(i) ? (object)"" : r.GetValue(i); } } } 出.Add(new object[] { 表名, rowid, cols.ToArray(), vals }); } /// 按 rowid 读一行全部列,追加到 出。[表名,rowid,列名[],值字典]。 private void 读回行(System.Data.SQLite.SQLiteConnection conn, string 表名, long rowid, List 出) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT rowid, * FROM \"" + 表名.Replace("\"", "") + "\" WHERE rowid=@r LIMIT 1;", conn)) { cmd.Parameters.AddWithValue("@r", rowid); using (var r = cmd.ExecuteReader()) { if (!r.Read()) return; var cols = new List(); var vals = new Dictionary(); for (int i = 1; i < r.FieldCount; i++) { cols.Add(r.GetName(i)); vals[r.GetName(i)] = r.IsDBNull(i) ? (object)"" : r.GetValue(i); } 出.Add(new object[] { 表名, rowid, cols.ToArray(), vals }); } } } /// 把读回的行回填内存缓存(按 _rowid_ 定位 DataRow;图号/箱号/元件三缓存都试)。 private void 回填缓存(object[][] 行集) { bool any = false; foreach (object[] 行 in 行集) { string tbl = (string)行[0]; long rowid = Convert.ToInt64(行[1]); string[] cols = (string[])行[2]; var vals = (Dictionary)行[3]; DataTable target = null; if (_tuhaoTable != null && _tuhaoTable.TableName == tbl) target = _tuhaoTable; else if (_xianghaoTable != null && _xianghaoTable.TableName == tbl) target = _xianghaoTable; else if (_yuanjianTable != null && _yuanjianTable.TableName == tbl) target = _yuanjianTable; if (target == null) continue; // 非当前显示的缓存表(在 _xianghaoCache/_yuanjianCache 里)不回填,下次 Reload 读库即新值 // ★★ 一劳永逸(2026-09-30 定稿):箱号表/图号表的内存回填【永久禁用】—— // 显示更新只走"刷新箱号表显示从库"(防抖重算后按内部ID对齐,一次到位)。 // 回填的 rowid/时序/选中残留不可靠,是价格跳动与错位显示的历史来源。 if (target == _xianghaoTable || target == _tuhaoTable) { continue; } foreach (DataRow dr in target.Rows) { if (!target.Columns.Contains("_rowid_")) break; // ★ 修复(2026-09-30):优先按"内部ID"匹配——行移动保存是全表 DELETE+INSERT 重建, // 库 rowid 会全部重排,而内存表的 _rowid_ 还是加载时的旧值, // 按 rowid 匹配会把更新回填到错误的行(如 AH4 的价格显示到 AH3 上)。 // 内部ID 不随重建变化,箱号/图号表都有该列;元件表无内部ID,退回 rowid 匹配。 bool rowMatch = false; object iidVal; if (target.Columns.Contains("内部ID") && vals.TryGetValue("内部ID", out iidVal) && iidVal != null) { string iid = Convert.ToString(iidVal).Trim(); if (!string.IsNullOrEmpty(iid) && string.Equals(Convert.ToString(dr["内部ID"]).Trim(), iid, StringComparison.OrdinalIgnoreCase)) { rowMatch = true; } } if (!rowMatch) { long rid = dr["_rowid_"] is long ? Convert.ToInt64(dr["_rowid_"]) : 0; if (rid != rowid) continue; } foreach (string c in cols) { if (!target.Columns.Contains(c)) continue; object v = vals.ContainsKey(c) ? vals[c] : null; dr[c] = (v == null) ? DBNull.Value : v; } any = true; break; } } if (any) { this.advancedDataGridView9.Invalidate(); this.advancedDataGridView4.Invalidate(); this.advancedDataGridView5.Invalidate(); } } } }