Files
ctbjrj/成套报价软件/编辑直写库.cs
T

723 lines
29 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 编辑直写库:虚拟模式单元格提交后,按 rowid 直接 UPDATE 报价库(300ms 防抖聚合成单事务),
// 同事务读回触发器算好的派生值与上级聚合行,回填内存读缓存并刷新显示。
// 数据库是唯一数据源:内存表只读,不再有任何"保存回写"路径。
using System;
using System.Collections.Generic;
using System.Data;
using System.Windows.Forms;
namespace 成套报价软件
{
public partial class MainForm
{
/// <summary>待写库项:表名|rowid|列 → 值(后写覆盖先写)。</summary>
private readonly Dictionary<string, string[]> _待写项 = new Dictionary<string, string[]>();
/// <summary>防抖定时器(300ms):把窗口期内的编辑聚合成一个事务写库。</summary>
private Timer _直写Timer;
/// <summary>项目页同步防抖定时器(800ms):编辑报价三表后,把图号汇总刷进项目列表库
/// 并刷新项目页网格显示(箱柜台数/成本总价/报出总价/利润实时可见)。</summary>
private Timer _项目页同步Timer;
/// <summary>切项目页前等待报价页待写批完成:保证项目页读取的是已提交数据库值。
/// UI线程用 await 让 WinForms Timer/后台直写继续运行,不阻塞窗口。</summary>
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秒,继续刷新(请检查直写日志)");
}
/// <summary>编辑报价三表后调用:防抖启动项目页同步(连续编辑只刷一次)。</summary>
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();
}
/// <summary>当前 Reload 耗时测量(进 Reload 方法时重启)。</summary>
private readonly System.Diagnostics.Stopwatch _swReload = System.Diagnostics.Stopwatch.StartNew();
/// <summary>公式表缓存(管线建触发器时用;图号粘贴管线开头加载)。</summary>
private List<公式条目> _公式_元件, _公式_箱号, _公式_图号;
/// <summary>待建触发器清单 [yj表, 箱号表, 箱号ID, 图号ID](数据写完后统一建,避免事务内 DDL 拖慢批量 INSERT)。</summary>
private readonly System.Collections.Generic.List<string[]> _待建yj触发器 = new System.Collections.Generic.List<string[]>();
/// <summary>待建箱号表触发器 [箱号表, 图号ID]。</summary>
private readonly System.Collections.Generic.List<string[]> _待建xh触发器 = new System.Collections.Generic.List<string[]>();
/// <summary>进项目时清理历史版本遗留的触发器(触发器方案已退役,应用层集中刷新)。后台执行。</summary>
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); }
});
}
/// <summary>保存按钮(功能区"保存"):实时架构下数据已在库,只刷跨库项目汇总并提示。</summary>
private void 保存按钮_实时保存提示()
{
try { 计算引擎.刷新项目汇总(GetQuoteDbPath(_currentProjectId), _currentProjectId, TuhaoTableName); }
catch (Exception ex) { Log("[保存] 刷新项目汇总异常: " + ex.Message); }
SetStatus("数据已实时保存在数据库");
MessageBox.Show(this, "数据已实时保存在数据库:\n编辑即写库,价格由数据库触发器自动计算,无需手动保存。",
"保存", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
/// <summary>
/// ★ Ctrl+S 强制保存(2026-09-30):报价页/选型页通用快捷键。
/// ★ G4(2026-10-01)新增:Ctrl+N 新建项目 / Ctrl+O 打开项目表选中项目 / Ctrl+Z 撤销删除。
/// 拦截窗体级命令键(比 KeyDown 早,且能抢在菜单/表格默认行为之前)。
/// </summary>
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);
}
/// <summary>
/// Ctrl+S 强制保存链:① 结束所有表格编辑态(编辑中的值进直写队列)
/// ② 直写队列同步落库 ③ 选型页未落库修改回写 ④ 刷报价三表+项目汇总 ⑤ 状态栏提示。
/// 与保存按钮不同:不弹 MessageBox,保存结果只在状态栏提示(不打断操作)。
/// </summary>
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"));
}
/// <summary>单元格提交后调用:入队待写项(库中无 rowid 的行跳过——如尚未 Reload 的新插入行)。</summary>
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();
}
/// <summary>在途直写批的写库任务(仅"写库"部分,纯后台完成,不依赖 UI 消息——强制落库可安全 Wait 等它)。</summary>
private System.Threading.Tasks.Task _直写写库Task;
/// <summary>直写队列强制落库(关窗/保存/AI写库前调用):同步驱动直到队列清空。
/// ★ B1改造(2026-10-01):原实现 DoEvents+Sleep 轮询 120×50ms 泵消息等异步回调推进,
/// 最坏卡界面 6 秒且依赖消息泵;现改为①等在途异步批的写库部分(纯后台 Task,Wait 不死锁)
/// ②残留批在 UI 线程同步直写(批是防抖聚合后的少量 UPDATE,单事务毫秒级)。
/// 全程不泵消息、不依赖 Timer/续体,签名不变(6 处调用方零改动)。</summary>
public void 直写队列强制落库()
{
for (int i = 0; i < 200; i++)
{
List<string[]> 批 = null;
lock (_待写项)
{
if (_待写项.Count > 0 && !_直写进行中)
{
批 = new List<string[]>(_待写项.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<string[]> 批;
lock (_待写项)
{
批 = new List<string[]>(_待写项.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<bool>();
_直写写库Task = 写库完成.Task;
// 表名→受影响 rowid(读回用)
var 受影响 = new Dictionary<string, HashSet<long>>();
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<long> set;
if (!受影响.TryGetValue(tbl, out set)) { set = new HashSet<long>(); 受影响[tbl] = set; }
set.Add(long.Parse(rowid));
}
tx.Commit();
}
}
}
catch (Exception exWrite)
{
// ★ 带批详情定位毒 SQL(表.列清单+完整异常),修完可留作排障
string 批详情 = "";
try
{
var parts = new List<string>();
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<object[]>();
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);
}
}
/// <summary>同步执行一批直写 UPDATE(单事务;强制落库路径用,UI线程直跑——
/// 批是防抖聚合后的少量 UPDATE,毫秒级)。返回 表名→受影响rowid 集;失败返回 null(内部已记日志)。</summary>
private Dictionary<string, HashSet<long>> 执行写库批(string dbPath, List<string[]> 批)
{
var 受影响 = new Dictionary<string, HashSet<long>>();
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<long> set;
if (!受影响.TryGetValue(tbl, out set)) { set = new HashSet<long>(); 受影响[tbl] = set; }
set.Add(long.Parse(rowid));
}
tx.Commit();
}
}
return 受影响;
}
catch (Exception exWrite)
{
string 批详情 = "";
try
{
var parts = new List<string>();
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;
}
}
/// <summary>直写批提交后的公共收尾:触发项目页汇总同步 + 读回派生/聚合行回填缓存。</summary>
private void 直写批后处理(string pid, string dbPath, Dictionary<string, HashSet<long>> 受影响)
{
// 批已提交落库,此刻 SUM 图号表才是新值
启动项目页同步();
try
{
var 读回 = new List<object[]>();
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);
}
}
/// <summary>为库内全部数据表补"内部ID"索引(幂等,2026-10-01 B3):
/// 直写 UPDATE / 箱号行聚合 / 重算定位大量 WHERE "内部ID" 查询,无索引则每次全表扫描。
/// 静态:两处调用点(项目库自检/方案库打开)有静态上下文。</summary>
private static void 确保数据表性能索引(System.Data.SQLite.SQLiteConnection conn)
{
try
{
var 表们 = new List<string>();
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); }
}
/// <summary>查上级行:元件表→(箱号表行, 图号行);箱号表→图号行。返回 [表名,rowid] 列表。</summary>
private List<object[]> 查上级(System.Data.SQLite.SQLiteConnection conn, string pid, string 表名, List<object[]> 已读)
{
var result = new List<object[]>();
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<object[]> 出)
{
if (string.IsNullOrEmpty(内部ID)) return;
var cols = new List<string>();
long rowid = 0;
var vals = new Dictionary<string, object>();
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 });
}
/// <summary>按 rowid 读一行全部列,追加到 出。[表名,rowid,列名[],值字典]。</summary>
private void 读回行(System.Data.SQLite.SQLiteConnection conn, string 表名, long rowid, List<object[]> 出)
{
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<string>();
var vals = new Dictionary<string, object>();
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 });
}
}
}
/// <summary>把读回的行回填内存缓存(按 _rowid_ 定位 DataRow;图号/箱号/元件三缓存都试)。</summary>
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<string, object>)行[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();
}
}
}
}