using System.Text.RegularExpressions; using System.Xml.Linq; namespace Rmsys.Infrastructure; /// /// 干部任免审批表 3.0(.lrmx)解析器。 /// /// 格式实测(海东样本 任免审批表模板.lrmx,330KB,UTF-8 带 BOM): /// <Person> 下 34 个拼音字段 + <JiaTingChengYuan> 多个 <Item> + ZhaoPian(照片 base64) /// 日期字段为空时可能写 000101;简历为一段长文本,按「1986.10--1989.09 内容」分段。 /// 本类只做「文件 → 内存对象」,不碰数据库,便于单测与 dry-run。 /// public class LrmxPerson { public string? Name, Sex, Birth, Nation, NativePlace, BirthPlace, PartyDate, WorkDate; public string? Health, TechPost, Expertise; public string? EduFullLevel, EduFullDegree, EduFullSchool, EduFullDegreeSchool; public string? EduInLevel, EduInDegree, EduInSchool, EduInDegreeSchool; public string? CurrentPost, ProposedPost, DismissPost; public string? Rewards, Exams, AppointReason; public string? ReportDept, FillDate, Filler, IdCard, Version; public byte[]? Photo; public string? PhotoExt; public List Resumes = new(); public List Family = new(); } public class LrmxResume { public int Sort; /// 如 1986.10--1989.09 public string Period = ""; public string Content = ""; } public class LrmxFamily { public int Sort; public string? Relation, Name, Birth, Political, WorkUnit; } public static class LrmxImporter { /// 人眼可读的报告,用于 dry-run 输出与导入回执 public static string Describe(LrmxPerson p) { var sb = new System.Text.StringBuilder(); void L(string k, string? v) => sb.AppendLine($" {k,-12}{v ?? ""}"); L("姓名", p.Name); L("性别", p.Sex); L("出生年月", p.Birth); L("民族", p.Nation); L("籍贯", p.NativePlace); L("出生地", p.BirthPlace); L("入党时间", p.PartyDate); L("参加工作", p.WorkDate); L("健康状况", p.Health); L("专业技术职务", p.TechPost); L("熟悉专业专长", p.Expertise); L("全日制", $"{p.EduFullLevel} / {p.EduFullDegree} / {p.EduFullSchool} / {p.EduFullDegreeSchool}"); L("在职教育", $"{p.EduInLevel} / {p.EduInDegree} / {p.EduInSchool} / {p.EduInDegreeSchool}"); L("现任职务", p.CurrentPost); L("拟任职务", p.ProposedPost); L("拟免职务", p.DismissPost); L("奖惩情况", p.Rewards); L("年度考核", p.Exams); L("任免理由", p.AppointReason); L("呈报单位", p.ReportDept); L("填表人", p.Filler); L("填表时间", p.FillDate); L("身份证", string.IsNullOrEmpty(p.IdCard) ? "(空)" : p.IdCard.Substring(0, Math.Min(6, p.IdCard.Length)) + "********"); L("版本", p.Version); L("照片", p.Photo == null ? "(无)" : $"{p.Photo.Length / 1024} KB {p.PhotoExt}"); sb.AppendLine($" 简历 {p.Resumes.Count} 段 / 家庭主要成员 {p.Family.Count} 人"); foreach (var r in p.Resumes) sb.AppendLine($" {r.Sort}. {r.Period} {Crop(r.Content, 40)}"); foreach (var f in p.Family) sb.AppendLine($" {f.Relation} {f.Name}({f.Birth}){f.Political} {Crop(f.WorkUnit, 30)}"); return sb.ToString(); } private static string Crop(string? s, int n) => string.IsNullOrEmpty(s) ? "" : (s.Length <= n ? s : s[..n] + "…"); public static LrmxPerson ParseFile(string path) => ParseText(File.ReadAllText(path)); public static LrmxPerson ParseText(string xml) { var doc = XDocument.Parse(xml); var root = doc.Root ?? throw new InvalidDataException("文件为空或不是有效 XML"); if (root.Name.LocalName != "Person") throw new InvalidDataException("不是任免审批表 3.0 文件(根节点应为 Person,实际 " + root.Name.LocalName + ")"); string? V(string tag) => root.Element(tag)?.Value?.Trim() is { Length: > 0 } s ? s : null; var p = new LrmxPerson { Name = V("XingMing"), Sex = V("XingBie"), Birth = Date6(V("ChuShengNianYue")), Nation = V("MinZu"), NativePlace = V("JiGuan"), BirthPlace = V("ChuShengDi"), PartyDate = Date6(V("RuDangShiJian")), WorkDate = Date6(V("CanJiaGongZuoShiJian")), Health = V("JianKangZhuangKuang"), TechPost = V("ZhuanYeJiShuZhiWu"), Expertise = V("ShuXiZhuanYeYouHeZhuanChang"), EduFullLevel = V("QuanRiZhiJiaoYu_XueLi"), EduFullDegree = V("QuanRiZhiJiaoYu_XueWei"), EduFullSchool = V("QuanRiZhiJiaoYu_XueLi_BiYeYuanXiaoXi"), EduFullDegreeSchool = V("QuanRiZhiJiaoYu_XueWei_BiYeYuanXiaoXi"), EduInLevel = V("ZaiZhiJiaoYu_XueLi"), EduInDegree = V("ZaiZhiJiaoYu_XueWei"), EduInSchool = V("ZaiZhiJiaoYu_XueLi_BiYeYuanXiaoXi"), EduInDegreeSchool = V("ZaiZhiJiaoYu_XueWei_BiYeYuanXiaoXi"), CurrentPost = V("XianRenZhiWu"), ProposedPost = V("NiRenZhiWu"), DismissPost = V("NiMianZhiWu"), Rewards = V("JiangChengQingKuang"), Exams = V("NianDuKaoHeJieGuo"), AppointReason = V("RenMianLiYou"), ReportDept = V("ChengBaoDanWei"), FillDate = Date6(V("TianBiaoShiJian")), Filler = V("TianBiaoRen"), IdCard = V("ShenFenZheng"), Version = V("Version"), }; // 简历:一段长文本,按「起止时间」切段(样本:1986.10--1989.09 步兵三师八团八连战士、班长 1989.09--…) var jl = V("JianLi"); if (!string.IsNullOrEmpty(jl)) p.Resumes = SplitResume(jl); // 家庭主要成员 var fam = root.Element("JiaTingChengYuan"); if (fam != null) { int i = 1; foreach (var it in fam.Elements("Item")) { string? F(string t) => it.Element(t)?.Value?.Trim() is { Length: > 0 } s ? s : null; p.Family.Add(new LrmxFamily { Sort = i++, Relation = F("ChengWei"), Name = F("XingMing"), Birth = Date6(F("ChuShengRiQi")), Political = F("ZhengZhiMianMao"), WorkUnit = F("GongZuoDanWeiJiZhiWu"), }); } } // 照片:base64,按文件头判 PNG / JPEG var zhaopian = V("ZhaoPian"); if (!string.IsNullOrEmpty(zhaopian)) { try { var b64 = Regex.Replace(zhaopian, @"\s", ""); var bytes = Convert.FromBase64String(b64); if (bytes.Length > 200) { p.Photo = bytes; p.PhotoExt = bytes.Length > 8 && bytes[0] == 0x89 && bytes[1] == 0x50 ? ".png" : bytes.Length > 3 && bytes[0] == 0xFF && bytes[1] == 0xD8 ? ".jpg" : ".bin"; } } catch { /* 照片坏了不影响主体导入 */ } } if (string.IsNullOrWhiteSpace(p.Name)) throw new InvalidDataException("文件里没有姓名(XingMing),无法导入"); return p; } /// 196804 → 1968.04;000101 之类的占位视为空 private static string? Date6(string? s) { if (string.IsNullOrWhiteSpace(s)) return null; s = s.Trim(); if (s is "000101" or "00000000" or "0" or "000000") return null; if (Regex.IsMatch(s, @"^\d{6}$")) return s[..4] + "." + s[4..]; if (Regex.IsMatch(s, @"^\d{8}$")) return s[..4] + "." + s[4..6]; return s; } // 允许「2012.07--」这种至今未写结束时间的写法 private static readonly Regex PeriodRe = new(@"\d{4}[\.\-/]\d{1,2}\s*(?:--|—|–|~|至)+\s*(?:\d{4}[\.\-/]\d{1,2}|至今|现在)?", RegexOptions.Compiled); /// 把简历长文本按起止时间切段;末段收尾。识别不到时间就整段作为一条。 public static List SplitResume(string text) { var list = new List(); var ms = PeriodRe.Matches(text); if (ms.Count == 0) { list.Add(new LrmxResume { Sort = 1, Period = "", Content = text.Trim() }); return list; } // 收集真正的分段起点:跳过「(其间:2007.03-2008.01 …)」这类嵌在上一段里的经历 var cuts = new List<(int at, int len)>(); foreach (Match m in ms) { var back = text.Substring(Math.Max(0, m.Index - 12), Math.Min(12, m.Index)); if (back.Contains("其间") || back.TrimEnd().EndsWith("(") || back.TrimEnd().EndsWith("(")) continue; cuts.Add((m.Index, m.Length)); } if (cuts.Count == 0) { list.Add(new LrmxResume { Sort = 1, Period = "", Content = text.Trim() }); return list; } // 被跳过的嵌套段(如「(其间:…)」)要补回上一段正文,否则会丢内容 for (int i = 0; i < cuts.Count; i++) { var period = Regex.Replace(text.Substring(cuts[i].at, cuts[i].len), @"\s+", ""); int start = cuts[i].at + cuts[i].len; int end = i + 1 < cuts.Count ? cuts[i + 1].at : text.Length; var content = text[start..end].Trim().Trim(',', ',', ';', ';'); list.Add(new LrmxResume { Sort = i + 1, Period = period, Content = content }); } return list; } }