// 计算引擎:公式表(Excel 式,存 fak.db"计算公式"表)+表达式→SQL 翻译+应用层集中刷新。 // 架构:数据库是唯一数据源。编辑/粘贴/删除直接写库,由**应用层**在同一事务里执行 // 公式生成的 SQL 刷新派生列与箱号/图号聚合(不用触发器——触发器对象随数据线性膨胀, // schema 解析与 DDL 成本把粘贴拖到秒级;应用层集中 SQL 效果相同且零膨胀)。 // 公式仍可配置:改 fak.db 公式表即生效(刷新时现读),无需改代码。 // 语法:数字 [列名] + - * / ( ) IF(条件,a,b) SUM('下层列名')(层级决定下层表) using System; using System.Collections.Generic; using System.Text; namespace 成套报价软件 { /// 一条公式:某层级某目标列的表达式。 public class 公式条目 { public string 层级; public string 目标列; public string 表达式; public int 排序; } public static class 计算引擎 { /// 日志委托(主程序启动时注入,接到 diag 日志;未注入时走 Trace)。 public static Action 日志 = delegate(string m) { System.Diagnostics.Trace.WriteLine("[计算引擎] " + m); }; private static void LogStatic(string msg) { try { 日志(msg); } catch { } } public const string 公式表名 = "计算公式"; /// 默认公式(与历史硬编码一致)。SUM('列')=对下层表该列求和。 public static readonly string[][] 默认公式 = new string[][] { new[]{"元件","采购单价","[表价]*[采购系数]"}, new[]{"元件","采购价","[采购单价]*[数量]"}, new[]{"元件","报出单价","[采购单价]*[报出系数]"}, new[]{"元件","报出价","[报出单价]*[数量]"}, new[]{"箱号","单台成本","SUM('采购价')"}, new[]{"箱号","成本价","[单台成本]*[数量]"}, new[]{"箱号","单台报出","SUM('报出价')"}, new[]{"箱号","报出价","[单台报出]*[数量]"}, new[]{"箱号","利润","[报出价]-[成本价]"}, new[]{"箱号","利润率","IF([成本价]=0,0,[利润]/[成本价])"}, new[]{"箱号","毛利率","IF([报出价]=0,0,[利润]/[报出价])"}, new[]{"图号","箱柜数量","SUM('数量')"}, new[]{"图号","单价","SUM('报出价')"}, new[]{"图号","总价","[单价]*[台数]"}, new[]{"图号","成本总价","SUM('成本价')*[台数]"}, new[]{"图号","毛利","[总价]-[成本总价]"}, new[]{"图号","毛利率","IF([总价]=0,0,[毛利]/[总价])"}, }; /// 方案库(fak.db)建公式表+写入默认公式(仅当表为空)。表已存在且非空则不动。 public static void EnsureFormulaTable(System.Data.SQLite.SQLiteConnection conn) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "CREATE TABLE IF NOT EXISTS \"" + 公式表名 + "\" (" + "层级 TEXT, 目标列 TEXT, 表达式 TEXT, 排序 INTEGER, 启用 INTEGER DEFAULT 1);", conn)) cmd.ExecuteNonQuery(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COUNT(*) FROM \"" + 公式表名 + "\";", conn)) { object o = cmd.ExecuteScalar(); if (o != null && Convert.ToInt64(o) > 0) return; } int sort = 0; foreach (var row in 默认公式) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "INSERT INTO \"" + 公式表名 + "\" (层级,目标列,表达式,排序,启用) VALUES (@l,@c,@e,@s,1);", conn)) { cmd.Parameters.AddWithValue("@l", row[0]); cmd.Parameters.AddWithValue("@c", row[1]); cmd.Parameters.AddWithValue("@e", row[2]); cmd.Parameters.AddWithValue("@s", ++sort); cmd.ExecuteNonQuery(); } } } /// 加载某层级启用中的公式(按排序)。加载失败(表缺/异常)回退默认公式。 public static List<公式条目> 加载公式(System.Data.SQLite.SQLiteConnection fakConn, string 层级) { var list = new List<公式条目>(); try { using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 目标列,表达式 FROM \"" + 公式表名 + "\" WHERE 层级=@l AND 启用=1 ORDER BY 排序;", fakConn)) { cmd.Parameters.AddWithValue("@l", 层级); using (var r = cmd.ExecuteReader()) { while (r.Read()) { if (r.IsDBNull(0) || r.IsDBNull(1)) continue; list.Add(new 公式条目 { 层级 = 层级, 目标列 = r.GetString(0), 表达式 = r.GetString(1), 排序 = 0 }); } } } if (list.Count > 0) return list; } catch { } foreach (var row in 默认公式) if (row[0] == 层级) list.Add(new 公式条目 { 层级 = 层级, 目标列 = row[1], 表达式 = row[2], 排序 = 0 }); return list; } // ============ 表达式解析(递归下降,线性扫描翻译) ============ private class 解析器 { public string 源; public int 位; public 解析器(string s) { 源 = s; 位 = 0; } public string 剩余表达式() { var sb = new StringBuilder(); while (位 < 源.Length) { char c = 源[位]; if (c == '[') { int end = 源.IndexOf(']', 位); if (end < 0) throw new Exception("表达式缺少 ]"); string col = 源.Substring(位 + 1, end - 位 - 1).Trim(); sb.Append(列引用(col)); 位 = end + 1; } else if (char.ToLower(源[位]) == 's' && 匹配关键字("SUM")) { 位 += 3; 跳过空格(); if (位 >= 源.Length || 源[位] != '(') throw new Exception("SUM 后应有 ("); 位++; 跳过空格(); if (位 >= 源.Length || 源[位] != '\'') throw new Exception("SUM 参数应为 '列名'"); int e2 = 源.IndexOf('\'', 位 + 1); if (e2 < 0) throw new Exception("SUM 参数缺少闭合引号"); string col = 源.Substring(位 + 1, e2 - 位 - 1); 位 = e2 + 1; 跳过空格(); if (位 < 源.Length && 源[位] == ')') 位++; sb.Append(Sum子查询(col)); } else if (char.ToLower(c) == 'i' && 匹配关键字("IF")) { 位 += 2; 跳过空格(); if (位 >= 源.Length || 源[位] != '(') throw new Exception("IF 后应有 ("); 位++; var parts = new List(); int depth = 1, start = 位; for (; 位 < 源.Length; 位++) { char d = 源[位]; if (d == '(') depth++; else if (d == ')') { depth--; if (depth == 0) { parts.Add(源.Substring(start, 位 - start)); 位++; break; } } else if (d == ',' && depth == 1) { parts.Add(源.Substring(start, 位 - start)); start = 位 + 1; } } if (parts.Count != 3) throw new Exception("IF 需要 3 个参数"); sb.Append("(CASE WHEN ").Append(子翻译(parts[0])) .Append(" THEN ").Append(子翻译(parts[1])) .Append(" ELSE ").Append(子翻译(parts[2])).Append(" END)"); } else if (c == '\'') { int e2 = 源.IndexOf('\'', 位 + 1); sb.Append(源.Substring(位, e2 - 位 + 1).Replace("'", "''")); 位 = e2 + 1; } else if (c == '=' || c == '<' || c == '>') { if (位 + 1 < 源.Length && (源[位 + 1] == '=' || 源[位 + 1] == '>')) { sb.Append(c).Append(源[位 + 1]); 位 += 2; } else { sb.Append('='); 位++; } } else if (char.IsDigit(c) || c == '.') { int s0 = 位; while (位 < 源.Length && (char.IsDigit(源[位]) || 源[位] == '.')) 位++; sb.Append(源.Substring(s0, 位 - s0)); } else if (c == '+' || c == '-' || c == '*' || c == '/' || c == '(' || c == ')' || c == ',') { sb.Append(c); 位++; } else 跳过空格(); } return sb.ToString(); } private string 子翻译(string sub) { var p = new 解析器(sub); p.引用模式 = 引用模式; p.下层表 = 下层表; p.层内公式 = 层内公式; p.展开栈 = 展开栈; return "(" + p.剩余表达式() + ")"; } public string 引用模式 = "NEW"; // NEW=触发器/行更新 NEW."列" / ROW=目标行 "列" public string 下层表 = ""; // SUM 的下层表名 public Dictionary 层内公式; // 目标列→公式(层内引用展开) public HashSet 展开栈 = new HashSet(); // 环检测 private bool 匹配关键字(string kw) { if (位 + kw.Length > 源.Length) return false; if (string.Compare(源, 位, kw, 0, kw.Length, true) != 0) return false; int nxt = 位 + kw.Length; if (nxt < 源.Length && (char.IsLetterOrDigit(源[nxt]) || 源[nxt] == '_')) return false; return true; } private void 跳过空格() { while (位 < 源.Length && char.IsWhiteSpace(源[位])) 位++; } /// 列引用:层内公式则展开(递归),否则基础列(带 CAST+COALESCE)。 private string 列引用(string col) { 公式条目 f; if (层内公式 != null && 层内公式.TryGetValue(col, out f) && !展开栈.Contains(col)) { 展开栈.Add(col); try { var p = new 解析器(f.表达式); p.引用模式 = 引用模式; p.下层表 = 下层表; p.层内公式 = 层内公式; p.展开栈 = 展开栈; return "(" + p.剩余表达式() + ")"; } finally { 展开栈.Remove(col); } } string q = 引用模式 == "NEW" ? "NEW." : ""; return "COALESCE(CAST(" + q + "\"" + col.Replace("\"", "\"\"") + "\" AS REAL),0)"; } /// SUM('列') → 对下层表整表求和(每箱号一张元件表/每图号一张箱号表,无条件)。 private string Sum子查询(string col) { return "COALESCE((SELECT CAST(SUM(CAST(\"" + col.Replace("\"", "\"\"") + "\" AS REAL)) AS REAL) FROM \"" + 下层表.Replace("\"", "\"\"") + "\"),0)"; } } /// 箱号层 SUM('列') → 箱号行上对应的聚合列(由元件触发器/应用层刷新维护)。 public static readonly System.Collections.Generic.Dictionary 箱号聚合列映射 = new System.Collections.Generic.Dictionary { { "采购价", "单台成本" }, { "报出价", "单台报出" } }; /// 翻译一条公式为 SQL 片段。失败返回 null 并记原因(调用方回退默认公式)。 private static string 翻译公式(string 表达式, Dictionary 层内公式, string 引用模式, string 下层表, out string 错误) { 错误 = null; try { var p = new 解析器(表达式) { 引用模式 = 引用模式, 下层表 = 下层表, 层内公式 = 层内公式, 展开栈 = new HashSet() }; return p.剩余表达式(); } catch (Exception ex) { 错误 = ex.Message; return null; } } /// 一层公式的 SET 片段(层内引用嵌套展开)。 /// 生成聚合公式 SET 片段(SUM 可映射到本行聚合列,AGGCOL 场景)。 public static string 生成聚合Set(List<公式条目> 公式, string 引用模式, string 下层表, Dictionary Sum映射 = null) { return 生成Set片段(公式, 引用模式, 下层表, Sum映射); } public static string 生成Set片段(List<公式条目> 公式, string 引用模式, string 下层表) { return 生成Set片段(公式, 引用模式, 下层表, null); } public static string 生成Set片段(List<公式条目> 公式, string 引用模式, string 下层表, Dictionary Sum映射) { var map = new Dictionary(); foreach (var f in 公式) map[f.目标列] = f; var sb = new StringBuilder(); foreach (var f in 公式) { string err; string sql = 翻译公式(f.表达式, map, 引用模式, 下层表, out err); if (sql == null) { foreach (var d in 默认公式) if (d[0] == f.层级 && d[1] == f.目标列) { string e2; sql = 翻译公式(d[2], map, 引用模式, 下层表, out e2); break; } if (sql == null) continue; LogStatic("公式[" + f.层级 + "." + f.目标列 + "]解析失败回退默认: " + err); } if (sb.Length > 0) sb.Append(", "); sb.Append("\"").Append(f.目标列).Append("\"=").Append(sql); } return sb.ToString(); } // ============ 应用层集中刷新(替代触发器) ============ private static void Exec(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string sql) { using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn, tx)) cmd.ExecuteNonQuery(); } /// 查表索引:类型+图号(+箱号) → 实际表名。 private static string 查表(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string 类型, string 图号, string 箱号) { using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 类型=@t AND 图号=@th" + (箱号 != null ? " AND 箱号=@xh" : "") + " LIMIT 1;", conn, tx)) { cmd.Parameters.AddWithValue("@t", 类型); cmd.Parameters.AddWithValue("@th", 图号); if (箱号 != null) cmd.Parameters.AddWithValue("@xh", 箱号); object o = cmd.ExecuteScalar(); return o == null || o == DBNull.Value ? null : o.ToString(); } } private static string Fak路径(string 项目库路径) { return System.IO.Path.Combine(System.IO.Path.GetDirectoryName(项目库路径), "..", "fak.db"); } /// 刷新一张元件表的全部派生列(集合 UPDATE,一条 SQL)。 public static void 刷新元件表派生(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string 项目库路径, string 元件表) { using (var fakConn = new System.Data.SQLite.SQLiteConnection("Data Source=" + Fak路径(项目库路径) + ";Version=3;")) { fakConn.Open(); string set = 生成Set片段(加载公式(fakConn, "元件"), "ROW", null); if (set.Length > 0) Exec(conn, tx, "UPDATE \"" + 元件表 + "\" SET " + set + ";"); } } /// 刷新一张元件表的单行派生列(编辑直写用;按 rowid 定位)。 public static void 刷新元件行派生(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string 项目库路径, string 元件表, long rowid) { using (var fakConn = new System.Data.SQLite.SQLiteConnection("Data Source=" + Fak路径(项目库路径) + ";Version=3;")) { fakConn.Open(); string set = 生成Set片段(加载公式(fakConn, "元件"), "ROW", null); if (set.Length > 0) Exec(conn, tx, "UPDATE \"" + 元件表 + "\" SET " + set + " WHERE rowid=" + rowid + ";"); } } /// 刷新箱号行聚合(单台成本/成本价/单台报出/报出价/利润/率;按行内部ID)。 public static void 刷新箱号行聚合(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string 项目库路径, string 图号ID, string 箱号ID, string 图号表名) { string yjTbl = 查表(conn, tx, "元件", 图号ID, 箱号ID); if (string.IsNullOrEmpty(yjTbl)) return; string xhTbl = 查表(conn, tx, "箱号", 图号ID, null); if (string.IsNullOrEmpty(xhTbl)) return; using (var fakConn = new System.Data.SQLite.SQLiteConnection("Data Source=" + Fak路径(项目库路径) + ";Version=3;")) { fakConn.Open(); string xhSet = 生成Set片段(加载公式(fakConn, "箱号"), "ROW", yjTbl); if (xhSet.Length > 0) Exec(conn, tx, "UPDATE \"" + xhTbl + "\" SET " + xhSet + " WHERE \"内部ID\"='" + 箱号ID.Replace("'", "''") + "';"); } } /// 刷新图号行聚合(箱柜数量/单价/总价/成本总价/毛利/率;按行内部ID)。 public static void 刷新图号行聚合(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string 项目库路径, string 图号ID, string 图号表名) { string xhTbl = 查表(conn, tx, "箱号", 图号ID, null); if (string.IsNullOrEmpty(xhTbl)) return; using (var fakConn = new System.Data.SQLite.SQLiteConnection("Data Source=" + Fak路径(项目库路径) + ";Version=3;")) { fakConn.Open(); string thSet = 生成Set片段(加载公式(fakConn, "图号"), "ROW", xhTbl); if (thSet.Length > 0) Exec(conn, tx, "UPDATE \"" + 图号表名 + "\" SET " + thSet + " WHERE \"内部ID\"='" + 图号ID.Replace("'", "''") + "';"); } } /// /// 批量刷新某图号的全部计算(粘贴/删除等批量操作后调用): /// 每张元件表派生列一条 UPDATE → 箱号每行聚合一条 → 图号行一条。全部集合 SQL。 /// public static void 批量刷新图号计算(System.Data.SQLite.SQLiteConnection conn, System.Data.SQLite.SQLiteTransaction tx, string 项目库路径, string 图号ID, string 图号表名) { string xhTbl = 查表(conn, tx, "箱号", 图号ID, null); if (string.IsNullOrEmpty(xhTbl)) return; var yj表们 = new List(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 实际表名 FROM 表索引 WHERE 类型='元件' AND 图号=@th;", conn, tx)) { cmd.Parameters.AddWithValue("@th", 图号ID); using (var r = cmd.ExecuteReader()) while (r.Read()) yj表们.Add(r.GetString(0)); } using (var fakConn = new System.Data.SQLite.SQLiteConnection("Data Source=" + Fak路径(项目库路径) + ";Version=3;")) { fakConn.Open(); string yjSet = 生成Set片段(加载公式(fakConn, "元件"), "ROW", null); var 箱号公式 = 加载公式(fakConn, "箱号"); string thSet = 生成Set片段(加载公式(fakConn, "图号"), "ROW", xhTbl); foreach (var yj in yj表们) if (yjSet.Length > 0) Exec(conn, tx, "UPDATE \"" + yj + "\" SET " + yjSet + ";"); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT 箱号, 实际表名 FROM 表索引 WHERE 类型='元件' AND 图号=@th;", conn, tx)) { cmd.Parameters.AddWithValue("@th", 图号ID); using (var r = cmd.ExecuteReader()) { var pairs = new List(); while (r.Read()) pairs.Add(new[] { r.IsDBNull(0) ? "" : r.GetString(0), r.IsDBNull(1) ? "" : r.GetString(1) }); foreach (var pr in pairs) { string 行Set = 生成Set片段(箱号公式, "ROW", pr[1]); if (行Set.Length > 0) Exec(conn, tx, "UPDATE \"" + xhTbl + "\" SET " + 行Set + " WHERE \"内部ID\"='" + pr[0].Replace("'", "''") + "';"); } } } if (thSet.Length > 0) Exec(conn, tx, "UPDATE \"" + 图号表名 + "\" SET " + thSet + " WHERE \"内部ID\"='" + 图号ID.Replace("'", "''") + "';"); } } /// 清理库内全部触发器。幂等。 /// 触发器方案已退役(应用层集中刷新),项目库/方案库都不应保留任何触发器; /// 整库复制的方案文件可能带入旧版fak.db模式的遗留触发器(引用已废弃的固定表"箱号", /// 命名不定),只清 trgjs_ 前缀清不掉 → 写入时报 no such table: 箱号。故全量清理。 public static void 清理遗留触发器(System.Data.SQLite.SQLiteConnection conn) { try { var old = new List(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT name FROM sqlite_master WHERE type='trigger';", conn)) using (var r = cmd.ExecuteReader()) while (r.Read()) old.Add(r.GetString(0)); if (old.Count == 0) return; using (var tx = conn.BeginTransaction()) { foreach (var nm in old) Exec(conn, tx, "DROP TRIGGER IF EXISTS \"" + nm.Replace("\"", "\"\"") + "\";"); tx.Commit(); } LogStatic("已清理触发器 " + old.Count + " 个"); } catch (Exception ex) { LogStatic("清理触发器失败: " + ex.Message); } } // ============ 项目汇总(跨库) ============ /// 从项目库图号表 SUM 后更新项目列表库汇总(读一条 + 写一条)。 public static void 刷新项目汇总(string 项目库路径, string 项目编号, string 图号表名) { if (string.IsNullOrWhiteSpace(项目编号)) return; if (!System.IO.File.Exists(项目库路径)) return; decimal 台数 = 0, 成本 = 0, 报出 = 0; using (var conn = new System.Data.SQLite.SQLiteConnection("Data Source=" + 项目库路径 + ";Version=3;Journal Mode=WAL;Synchronous=Normal;")) { conn.Open(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "SELECT COALESCE(SUM(CAST(\"箱柜数量\" AS REAL)),0), COALESCE(SUM(CAST(\"成本总价\" AS REAL)),0), " + "COALESCE(SUM(CAST(\"总价\" AS REAL)),0) FROM \"" + 图号表名 + "\";", conn)) using (var r = cmd.ExecuteReader()) if (r.Read()) { 台数 = Convert.ToDecimal(r.GetValue(0)); 成本 = Convert.ToDecimal(r.GetValue(1)); 报出 = Convert.ToDecimal(r.GetValue(2)); } } string projectsDb = System.IO.Path.Combine( System.IO.Path.GetDirectoryName(项目库路径), "..", "advancedDataGridView6Table.db"); projectsDb = System.IO.Path.GetFullPath(projectsDb); if (!System.IO.File.Exists(projectsDb)) return; decimal 利润 = 报出 - 成本; // ★ 口径(2026-09-29):利润率存小数(0.05=5%),与箱号表利润率口径一致 decimal 利润率 = 成本 == 0 ? 0 : Math.Round(利润 / 成本, 2); using (var conn = new System.Data.SQLite.SQLiteConnection("Data Source=" + projectsDb + ";Version=3;Journal Mode=WAL;Synchronous=Normal;")) { conn.Open(); using (var cmd = new System.Data.SQLite.SQLiteCommand( "UPDATE advancedDataGridView6Table SET 箱柜台数=@t, 成本总价=@c, 报出总价=@b, 利润=@p, 利润率=@r WHERE 项目编号=@id;", conn)) { cmd.Parameters.AddWithValue("@t", 台数.ToString("0.##")); cmd.Parameters.AddWithValue("@c", 成本.ToString("0.##")); cmd.Parameters.AddWithValue("@b", 报出.ToString("0.##")); cmd.Parameters.AddWithValue("@p", 利润.ToString("0.##")); cmd.Parameters.AddWithValue("@r", 利润率.ToString("0.##")); cmd.Parameters.AddWithValue("@id", 项目编号); cmd.ExecuteNonQuery(); } } } } }