Files
ctbjrj/成套报价软件/计算引擎.cs
T

509 lines
22 KiB
C#

// 计算引擎:公式表(Excel 式,存 fak.db"计算公式"表)+表达式→SQL 翻译+应用层集中刷新。
// 架构:数据库是唯一数据源。编辑/粘贴/删除直接写库,由**应用层**在同一事务里执行
// 公式生成的 SQL 刷新派生列与箱号/图号聚合(不用触发器——触发器对象随数据线性膨胀,
// schema 解析与 DDL 成本把粘贴拖到秒级;应用层集中 SQL 效果相同且零膨胀)。
// 公式仍可配置:改 fak.db 公式表即生效(刷新时现读),无需改代码。
// 语法:数字 [列名] + - * / ( ) IF(条件,a,b) SUM('下层列名')(层级决定下层表)
using System;
using System.Collections.Generic;
using System.Text;
namespace 成套报价软件
{
/// <summary>一条公式:某层级某目标列的表达式。</summary>
public class 公式条目
{
public string 层级;
public string 目标列;
public string 表达式;
public int 排序;
}
public static class 计算引擎
{
/// <summary>日志委托(主程序启动时注入,接到 diag 日志;未注入时走 Trace)。</summary>
public static Action<string> 日志 = delegate(string m) { System.Diagnostics.Trace.WriteLine("[计算引擎] " + m); };
private static void LogStatic(string msg) { try { 日志(msg); } catch { } }
public const string 公式表名 = "计算公式";
/// <summary>默认公式(与历史硬编码一致)。SUM('列')=对下层表该列求和。</summary>
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,[毛利]/[总价])"},
};
/// <summary>方案库(fak.db)建公式表+写入默认公式(仅当表为空)。表已存在且非空则不动。</summary>
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();
}
}
}
/// <summary>加载某层级启用中的公式(按排序)。加载失败(表缺/异常)回退默认公式。</summary>
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<string>();
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<string, 公式条目> 层内公式; // 目标列→公式(层内引用展开)
public HashSet<string> 展开栈 = new HashSet<string>(); // 环检测
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(源[位])) 位++; }
/// <summary>列引用:层内公式则展开(递归),否则基础列(带 CAST+COALESCE)。</summary>
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)";
}
/// <summary>SUM('列') → 对下层表整表求和(每箱号一张元件表/每图号一张箱号表,无条件)。</summary>
private string Sum子查询(string col)
{
return "COALESCE((SELECT CAST(SUM(CAST(\"" + col.Replace("\"", "\"\"") +
"\" AS REAL)) AS REAL) FROM \"" + 下层表.Replace("\"", "\"\"") + "\"),0)";
}
}
/// <summary>箱号层 SUM('列') → 箱号行上对应的聚合列(由元件触发器/应用层刷新维护)。</summary>
public static readonly System.Collections.Generic.Dictionary<string, string> 箱号聚合列映射 =
new System.Collections.Generic.Dictionary<string, string> { { "采购价", "单台成本" }, { "报出价", "单台报出" } };
/// <summary>翻译一条公式为 SQL 片段。失败返回 null 并记原因(调用方回退默认公式)。</summary>
private static string 翻译公式(string 表达式, Dictionary<string, 公式条目> 层内公式,
string 引用模式, string 下层表, out string 错误)
{
错误 = null;
try
{
var p = new 解析器(表达式)
{ 引用模式 = 引用模式, 下层表 = 下层表, 层内公式 = 层内公式, 展开栈 = new HashSet<string>() };
return p.剩余表达式();
}
catch (Exception ex) { 错误 = ex.Message; return null; }
}
/// <summary>一层公式的 SET 片段(层内引用嵌套展开)。</summary>
/// <summary>生成聚合公式 SET 片段(SUM 可映射到本行聚合列,AGGCOL 场景)。</summary>
public static string 生成聚合Set(List<公式条目> 公式, string 引用模式, string 下层表, Dictionary<string, string> Sum映射 = null)
{
return 生成Set片段(公式, 引用模式, 下层表, Sum映射);
}
public static string 生成Set片段(List<公式条目> 公式, string 引用模式, string 下层表)
{
return 生成Set片段(公式, 引用模式, 下层表, null);
}
public static string 生成Set片段(List<公式条目> 公式, string 引用模式, string 下层表, Dictionary<string, string> Sum映射)
{
var map = new Dictionary<string, 公式条目>();
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();
}
/// <summary>查表索引:类型+图号(+箱号) → 实际表名。</summary>
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");
}
/// <summary>刷新一张元件表的全部派生列(集合 UPDATE,一条 SQL)。</summary>
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 + ";");
}
}
/// <summary>刷新一张元件表的单行派生列(编辑直写用;按 rowid 定位)。</summary>
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 + ";");
}
}
/// <summary>刷新箱号行聚合(单台成本/成本价/单台报出/报出价/利润/率;按行内部ID)。</summary>
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("'", "''") + "';");
}
}
/// <summary>刷新图号行聚合(箱柜数量/单价/总价/成本总价/毛利/率;按行内部ID)。</summary>
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("'", "''") + "';");
}
}
/// <summary>
/// 批量刷新某图号的全部计算(粘贴/删除等批量操作后调用):
/// 每张元件表派生列一条 UPDATE → 箱号每行聚合一条 → 图号行一条。全部集合 SQL。
/// </summary>
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<string>();
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<string[]>();
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("'", "''") + "';");
}
}
/// <summary>清理库内全部触发器。幂等。
/// 触发器方案已退役(应用层集中刷新),项目库/方案库都不应保留任何触发器;
/// 整库复制的方案文件可能带入旧版fak.db模式的遗留触发器(引用已废弃的固定表"箱号",
/// 命名不定),只清 trgjs_ 前缀清不掉 → 写入时报 no such table: 箱号。故全量清理。</summary>
public static void 清理遗留触发器(System.Data.SQLite.SQLiteConnection conn)
{
try
{
var old = new List<string>();
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); }
}
// ============ 项目汇总(跨库) ============
/// <summary>从项目库图号表 SUM 后更新项目列表库汇总(读一条 + 写一条)。</summary>
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();
}
}
}
}
}