// 编辑直写库:虚拟模式单元格提交后,按 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