723 lines
29 KiB
C#
723 lines
29 KiB
C#
// 编辑直写库:虚拟模式单元格提交后,按 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();
|
||
}
|
||
}
|
||
}
|
||
}
|