using System.Security.Claims; using Microsoft.AspNetCore.Mvc; using Rmsys.Api.Compat; using Microsoft.EntityFrameworkCore; using Rmsys.Infrastructure; namespace Rmsys.Api.Endpoints; /// /// 干部任免审批表 3.0(.lrmx)导入接口 —— 后台「任免表导入 / 批量上传个人材料」用。 /// POST /api/admin/lrmx/import multipart: file(.lrmx) + meetingId + dryRun(0/1) /// dryRun=1 只解析并回执字段,不写库(向导里的「格式解析」预览步骤)。 /// public static class LrmxEndpoints { private const long MaxBytes = 20L * 1024 * 1024; // .lrmx 含照片,给 20MB public static void MapLrmxEndpoints(this WebApplication app) { app.MapPost("/api/admin/lrmx/import", async (HttpRequest req, ClaimsPrincipal p, RmsysDbContext db, IWebHostEnvironment env) => { // net6 不支持 IFormFile 参数绑定,统一手动读表单 var form = await FormCompat.ReadFormAsync(req); if (form == null) return Results.Ok(new { ok = false, message = "请求格式不正确(需 multipart/form-data)" }); var file = form.Files.GetFile("file"); var meetingId = FormCompat.Int(form, "meetingId"); var dryRun = FormCompat.Int(form, "dryRun"); if (file == null || file.Length == 0) return Results.Ok(new { ok = false, message = "未收到文件" }); if (file.Length > MaxBytes) return Results.Ok(new { ok = false, message = $"文件超过 20 MB(实际 {file.Length / 1024 / 1024} MB)" }); if (!string.Equals(Path.GetExtension(file.FileName), ".lrmx", StringComparison.OrdinalIgnoreCase)) return Results.Ok(new { ok = false, message = "只接受 .lrmx(干部任免审批表 3.0 导出文件)" }); // 会议可写校验(已登记 / 已归档不允许再导入) if (meetingId > 0) { var m = await db.Meetings.FirstOrDefaultAsync(x => x.Id == meetingId && x.IsDeleted == 0); if (m == null) return Results.Ok(new { ok = false, message = "会议不存在" }); var deny = Auth.MeetingGuard.CheckWritable(m); if (deny != null) return Results.Json(new { ok = false, message = deny }, statusCode: 409); } var tmp = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".lrmx"); try { await using (var fs = File.Create(tmp)) await file.CopyToAsync(fs); LrmxPerson person; try { person = LrmxImporter.ParseFile(tmp); } catch (Exception ex) { return Results.Ok(new { ok = false, message = "解析失败:" + ex.Message }); } if (dryRun == 1) { return Results.Ok(new { ok = true, dryRun = true, name = person.Name, sex = person.Sex, birth = person.Birth, nation = person.Nation, nativePlace = person.NativePlace, partyDate = person.PartyDate, workDate = person.WorkDate, currentPost = person.CurrentPost, proposedPost = person.ProposedPost, dismissPost = person.DismissPost, resumes = person.Resumes.Count, family = person.Family.Count, photo = person.Photo != null, version = person.Version, // 预览用:前若干段简历与家庭成员,便于干部科核对解析结果 resumeSample = person.Resumes.Take(3).Select(x => x.Period + " " + x.Content).ToList(), familySample = person.Family.Take(5) .Select(x => $"{x.Relation} {x.Name}({x.Birth}){x.Political} {x.WorkUnit}").ToList() }); } var user = p.FindFirst(ClaimTypes.Name)?.Value ?? "—"; var root = Path.Combine(env.ContentRootPath, "App_Data"); var r = await LrmxImportService.ImportAsync(db, person, meetingId, root, tmp, Path.GetFileName(file.FileName), user); return Results.Ok(new { ok = true, created = r.Created, cadreId = r.CadreId, name = r.Name, resumes = r.Resumes, family = r.Family, photo = r.Photo, onRoster = r.OnRoster, version = r.Version, message = (r.Created ? "新增干部 " : "更新干部 ") + r.Name + $"|简历 {r.Resumes} 段|家庭 {r.Family} 人" + (r.Photo ? "|照片已存" : "") }); } finally { try { if (File.Exists(tmp)) File.Delete(tmp); } catch { /* 临时文件删不掉不影响结果 */ } } }).RequireAuthorization() // .NET 8 的 [FromForm] 绑定会给端点挂上防伪校验元数据;本项目用 Bearer 令牌而非 Cookie, // 且未注册防伪中间件,这里显式关闭,否则请求直接 500。 .NoAntiforgery(); } }