using Microsoft.EntityFrameworkCore; using Rmsys.Infrastructure; // ============================================================ // Rmsys.Tools —— 运维/构建命令行 // 用法: // dotnet run --project src/Rmsys.Tools -- export-package --meeting 1 --out App_Data/packages/PK.db // dotnet run --project src/Rmsys.Tools -- verify-package --file App_Data/packages/PK.db // ============================================================ var cmd = args.Length > 0 ? args[0] : "help"; var opt = ParseOpts(args.Skip(1).ToArray()); string Opt(string key, string? def = null) => opt.TryGetValue(key, out var v) ? v : (def ?? throw new ArgumentException("缺少参数 --" + key)); var conn = Environment.GetEnvironmentVariable("RMSYS_MYSQL") ?? DbConfig.DefaultMySql; var options = new DbContextOptionsBuilder() .UseMySql(conn, DbConfig.ServerVersion) .Options; switch (cmd) { case "export-package": { var meetingId = int.Parse(Opt("meeting", "1")); var plain = opt.ContainsKey("plain"); var ext = plain ? ".db" : ".pkg"; var outPath = Opt("out", $"App_Data/packages/PK-{DateTime.Now:yyyyMMdd-HHmmss}{ext}"); await using var db = new RmsysDbContext(options); var exp = new PackageExporter(db); var r = plain ? await exp.ExportAsync(meetingId, outPath, "cli") : await exp.ExportEncryptedAsync(meetingId, outPath, "cli"); Console.WriteLine(r.Human()); break; } case "decrypt-package": { // 排障用:把 .pkg 解成可读的 SQLite,便于用 Navicat 看包内数据 var file = Opt("file"); var outPath = Opt("out", Path.ChangeExtension(file, ".db")); await using var db = new RmsysDbContext(options); var key = new PackageExporter(db).MasterKey(); var pkg = await File.ReadAllBytesAsync(file); var (hok, ver, flags, size) = PackageCrypto.ReadHeader(pkg); Console.WriteLine($" 头部:{(hok ? "有效" : "无效")} version={ver} flags={flags} 明文长度={size}"); var plain = PackageCrypto.Decode(pkg, key); await File.WriteAllBytesAsync(outPath, plain); Console.WriteLine($" 已解密:{plain.Length / 1024.0:F1} KB → {outPath}"); break; } case "keygen-package": { // 查看/生成数据包主密钥(轮换时:改库后必须同步重编 APK) await using var db = new RmsysDbContext(options); var key = new PackageExporter(db).MasterKey(); Console.WriteLine("数据包主密钥(base64): " + Convert.ToBase64String(key)); Console.WriteLine("⚠ 轮换密钥后,已发出的旧包全部作废,且必须重编 APK 内置同一把。"); break; } case "verify-package": { var file = Opt("file"); var r = PackageVerifier.Verify(file); Console.WriteLine(r.Human()); break; } case "self-test-crypto": { Console.WriteLine("国密算法自检(对照 GB/T 32907 SM4 与 GB/T 32905 SM3 官方测试向量)"); var (ok, report) = CryptoVectors.SelfTest(); Console.Write(report); Console.WriteLine(); Console.WriteLine("数据包加密封装自检"); var (ok2, report2) = PackageCrypto.SelfTest(); Console.Write(report2); var all = ok && ok2; Console.WriteLine(all ? "结论:全部通过 ✔" : "结论:存在不通过项 ✖"); Environment.ExitCode = all ? 0 : 1; break; } case "counts": { await using var db = new RmsysDbContext(options); Console.WriteLine("depts " + await db.Depts.CountAsync()); Console.WriteLine("meetings " + await db.Meetings.CountAsync()); Console.WriteLine("cadres " + await db.Cadres.CountAsync()); Console.WriteLine("resumes " + await db.CadreResumes.CountAsync()); Console.WriteLine("families " + await db.CadreFamilies.CountAsync()); Console.WriteLine("notes " + await db.CadreNotes.CountAsync()); Console.WriteLine("meetingcadres " + await db.MeetingCadres.CountAsync()); Console.WriteLine("materials " + await db.Materials.CountAsync()); Console.WriteLine("appointdocs " + await db.AppointDocs.CountAsync()); Console.WriteLine("paddevices " + await db.PadDevices.CountAsync()); break; } case "import-lrmx": { var file = Opt("file"); var dry = opt.ContainsKey("dry-run"); var meetingId = int.Parse(Opt("meeting", "0")); var person = Rmsys.Infrastructure.LrmxImporter.ParseFile(file); Console.WriteLine("解析结果(" + Path.GetFileName(file) + "):"); Console.Write(Rmsys.Infrastructure.LrmxImporter.Describe(person)); if (dry) { Console.WriteLine("(dry-run:只解析,未写库)"); break; } await using var db = new RmsysDbContext(options); var apiRoot = Path.Combine(Directory.GetCurrentDirectory(), "src", "Rmsys.Api", "App_Data"); Directory.CreateDirectory(apiRoot); var r = await Rmsys.Infrastructure.LrmxImportService.ImportAsync( db, person, meetingId, apiRoot, file, Path.GetFileName(file), "cli"); Console.WriteLine($"导入完成:{(r.Created ? "新增" : "更新")}干部 #{r.CadreId} {r.Name}" + $"|简历 {r.Resumes} 段|家庭 {r.Family} 人|照片 {(r.Photo ? "已存" : "无")}" + (r.OnRoster ? $"|材料版本 {r.Version}" : "|仅入干部库")); break; } default: Console.WriteLine(""" 干部任免上会系统 · 工具 export-package --meeting [--out ] [--plain] 导出数据包(默认国密SM4加密为 .pkg) --plain 导出明文 SQLite(仅排障用) decrypt-package --file [--out ] 解密数据包(排障用) keygen-package 打印/生成数据包主密钥 verify-package --file 校验数据包内容 self-test-crypto 国密算法 + 包格式自检 counts 统计各表行数 """); break; } static Dictionary ParseOpts(string[] a) { var d = new Dictionary(); for (int i = 0; i < a.Length; i++) { if (a[i].StartsWith("--")) { var k = a[i].Substring(2); var v = (i + 1 < a.Length && !a[i + 1].StartsWith("--")) ? a[++i] : "1"; d[k] = v; } } return d; }