using System.Text.RegularExpressions; using Microsoft.Data.Sqlite; namespace Rmsys.Infrastructure; /// /// 数据包校验:生成的 .db 能不能被 PAD 正常用,先在这里把关。 /// 检查项:文件完整性、配置齐全、行数自洽、中文未乱码、上会顺序连续。 /// public static class PackageVerifier { public static VerifyReport Verify(string path) { var rep = new VerifyReport { Path = path }; if (!File.Exists(path)) { rep.Fatal = "文件不存在"; return rep; } rep.Bytes = new FileInfo(path).Length; try { var csb = new SqliteConnectionStringBuilder { DataSource = path, Mode = SqliteOpenMode.ReadOnly }; using var conn = new SqliteConnection(csb.ConnectionString); conn.Open(); // 1) 表齐全 var tables = new HashSet(StringComparer.OrdinalIgnoreCase); using (var c = conn.CreateCommand()) { c.CommandText = "select name from sqlite_master where type='table'"; using var rd = c.ExecuteReader(); while (rd.Read()) tables.Add(rd.GetString(0)); } // pad_bound 是实际表名 foreach (var t in new[] { "pkg_config", "depts", "pad_bound", "cadres", "resumes", "families", "notes", "materials", "appointdocs", "dicts" }) { if (!tables.Contains(t)) rep.MissingTables.Add(t); } // 2) 配置 var cfg = new Dictionary(StringComparer.OrdinalIgnoreCase); using (var c = conn.CreateCommand()) { c.CommandText = "select k, v from pkg_config"; using var rd = c.ExecuteReader(); while (rd.Read()) cfg[rd.GetString(0)] = rd.IsDBNull(1) ? "" : rd.GetString(1); } foreach (var k in new[] { "pkgNo", "meetingName", "secret", "expireAt", "clearPolicy", "watermark", "fontLevel", "matOrder", "padBound", "cadreCount" }) { if (!cfg.ContainsKey(k) || string.IsNullOrWhiteSpace(cfg[k])) rep.MissingConfig.Add(k); } rep.PkgNo = cfg.GetValueOrDefault("pkgNo", ""); rep.MeetingName = cfg.GetValueOrDefault("meetingName", ""); rep.ExpireAt = cfg.GetValueOrDefault("expireAt", ""); rep.Watermark = cfg.GetValueOrDefault("watermark", ""); rep.MatOrder = cfg.GetValueOrDefault("matOrder", ""); // 3) 行数 rep.Cadres = Count(conn, "cadres"); rep.Resumes = Count(conn, "resumes"); rep.Families = Count(conn, "families"); rep.Notes = Count(conn, "notes"); rep.Materials = Count(conn, "materials"); if (tables.Contains("files")) { try { using var cf = conn.CreateCommand(); cf.CommandText = "select count(*), ifnull(sum(size),0) from files"; using var rf = cf.ExecuteReader(); if (rf.Read()) { rep.PdfFiles = rf.GetInt32(0); rep.PdfBytes = rf.GetInt64(1); } } catch { } try { using var df = conn.CreateCommand(); df.CommandText = "select count(*), ifnull(sum(size),0) from docfiles"; using var rd = df.ExecuteReader(); if (rd.Read()) { rep.DocFiles = rd.GetInt32(0); rep.DocBytes = rd.GetInt64(1); } } catch { } rep.DocLines = Count(conn, "doclines"); } rep.Docs = Count(conn, "appointdocs"); rep.PadBound = Count(conn, "pad_bound"); if (int.TryParse(cfg.GetValueOrDefault("cadreCount"), out var expect) && expect != rep.Cadres) rep.Errors.Add($"配置 cadreCount={expect} 与实际 {rep.Cadres} 不一致"); // 4) 上会顺序必须从 1 连续 var sorts = new List(); using (var c = conn.CreateCommand()) { c.CommandText = "select sortNo from cadres order by sortNo"; using var rd = c.ExecuteReader(); while (rd.Read()) sorts.Add(rd.GetInt32(0)); } for (int i = 0; i < sorts.Count; i++) if (sorts[i] != i + 1) { rep.Errors.Add($"上会顺序不连续:第 {i + 1} 位是 {sorts[i]}"); break; } // 5) 中文未乱码(姓名必须 2~4 个汉字) var badNames = new List(); using (var c = conn.CreateCommand()) { c.CommandText = "select name from cadres"; using var rd = c.ExecuteReader(); while (rd.Read()) { var n = rd.IsDBNull(0) ? "" : rd.GetString(0); if (!Regex.IsMatch(n, @"^[\u4e00-\u9fa5·]{2,8}$")) badNames.Add(n); } } if (badNames.Count > 0) rep.Errors.Add("姓名异常(疑似乱码):" + string.Join(",", badNames)); // 6) 每人至少一条简历(任免表「简历」行不能空) using (var c = conn.CreateCommand()) { c.CommandText = @"select count(*) from cadres cd where not exists(select 1 from resumes r where r.cadreId = cd.id)"; var n = Convert.ToInt32(c.ExecuteScalar()); if (n > 0) rep.Errors.Add($"{n} 名干部没有简历记录"); } // 7) 期望的材料类型是否都在清单里 if (!string.IsNullOrEmpty(rep.MatOrder)) { foreach (var t in new[] { "t30", "exam" }) if (!rep.MatOrder.Contains(t)) rep.Errors.Add("下发清单缺少必需材料类型:" + t); } rep.Ok = rep.MissingTables.Count == 0 && rep.MissingConfig.Count == 0 && rep.Errors.Count == 0; } catch (Exception e) { rep.Fatal = e.GetType().Name + ": " + e.Message; rep.Ok = false; } return rep; } private static int Count(SqliteConnection conn, string table) { try { using var c = conn.CreateCommand(); c.CommandText = "select count(*) from " + table; return Convert.ToInt32(c.ExecuteScalar()); } catch { return -1; } } } public class VerifyReport { public string Path { get; set; } = ""; public long Bytes { get; set; } public bool Ok { get; set; } public string? Fatal { get; set; } public string PkgNo { get; set; } = ""; public string MeetingName { get; set; } = ""; public string ExpireAt { get; set; } = ""; public string Watermark { get; set; } = ""; public string MatOrder { get; set; } = ""; public int Cadres { get; set; } /// 包内 PDF 正文份数与总字节 public int PdfFiles { get; set; } /// 包内任免文件原件份数(上传的 Word 转成的 PDF) public int DocFiles { get; set; } public long DocBytes { get; set; } /// 包内任免文件正文段落数(PAD 用文字渲染,人名可点) public int DocLines { get; set; } public long PdfBytes { get; set; } public int Resumes { get; set; } public int Families { get; set; } public int Notes { get; set; } public int Materials { get; set; } public int Docs { get; set; } public int PadBound { get; set; } public List MissingTables { get; } = new(); public List MissingConfig { get; } = new(); public List Errors { get; } = new(); public string Human() { var sb = new System.Text.StringBuilder(); sb.AppendLine(Ok ? "✔ 数据包校验通过" : "✘ 数据包校验未通过"); sb.AppendLine($" 文件 {Path} ({Bytes / 1024.0:F1} KB)"); if (Fatal != null) { sb.AppendLine(" 致命 " + Fatal); return sb.ToString(); } sb.AppendLine($" 包号 {PkgNo}"); sb.AppendLine($" 会议 {MeetingName}"); sb.AppendLine($" 有效期至 {ExpireAt}"); sb.AppendLine($" 水印 {Watermark}"); sb.AppendLine($" 下发清单 {MatOrder}"); sb.AppendLine($" 干部 {Cadres} / 简历 {Resumes} / 家庭 {Families} / 批注 {Notes} / 材料 {Materials} / 任免文件 {Docs} / 绑定设备 {PadBound}"); if (MissingTables.Count > 0) sb.AppendLine(" 缺表 " + string.Join(",", MissingTables)); if (MissingConfig.Count > 0) sb.AppendLine(" 缺配置 " + string.Join(",", MissingConfig)); foreach (var e in Errors) sb.AppendLine(" 问题 " + e); return sb.ToString(); } }