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;
}
}