using System.Security.Claims; using Microsoft.EntityFrameworkCore; using Rmsys.Domain; using Rmsys.Domain.Entities; using Rmsys.Infrastructure; namespace Rmsys.Api.Endpoints; /// /// 后台写操作接口(全部需要登录)。 /// 每个写操作都落一条审计日志 ——「登录、查看、修改、删除、导出、下发、清除全链路留痕」 /// 是保密检查的直接证据,不能只在界面上点一下就算了。 /// public static class WriteEndpoints { public static void MapWriteEndpoints(this WebApplication app) { // ============ 会议 ============ app.MapPost("/api/admin/meetings", async (MeetingReq req, ClaimsPrincipal p, RmsysDbContext db) => { if (string.IsNullOrWhiteSpace(req.name)) return Results.Ok(new { ok = false, message = "会议名称不能为空" }); if (!MeetingType.All.Contains(req.type ?? "")) return Results.Ok(new { ok = false, message = "会议类型非法" }); // P1-1 届次号必填:部务会 / 常委会必须填,书记专题会等不设届次的类型可空 if (MeetingType.HasSessionNo(req.type!) && !req.sessionNo.HasValue) return Results.Ok(new { ok = false, message = "请填写届次号(如 192 表示第192次)" }); if (req.sessionNo.HasValue && req.sessionNo.Value <= 0) return Results.Ok(new { ok = false, message = "届次号必须是大于 0 的整数" }); // P1-2 查重:同一类型下届次号不能重复;会议名称全量不能重复 var dupName = await db.Meetings.AnyAsync(x => x.IsDeleted == 0 && x.Name == req.name!.Trim()); if (dupName) return Results.Ok(new { ok = false, message = "会议名称已存在,请勿重复创建" }); if (req.sessionNo.HasValue) { var no = req.sessionNo.Value; var ty = req.type!; var dupNo = await db.Meetings.AnyAsync(x => x.IsDeleted == 0 && x.Type == ty && x.SessionNo == no); if (dupNo) return Results.Ok(new { ok = false, message = $"该类型下已有第 {no} 次会议,请核对届次号" }); } var m = new Meeting { Type = req.type!, SessionNo = req.sessionNo, Name = req.name!.Trim(), HeldAt = req.heldAt, Place = req.place, Host = req.host, Secret = string.IsNullOrWhiteSpace(req.secret) ? "机密" : req.secret!, SecretStars = StarsFor(req.secret), PwdLevel = string.IsNullOrWhiteSpace(req.pwdLevel) ? "B级" : req.pwdLevel!, Status = MeetingStatus.Draft, BatchNo = req.batchNo, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }; db.Meetings.Add(m); await db.SaveChangesAsync(); await Audit(db, p, "新建会议", m.Name, $"类型={m.Type} 届次={m.SessionNo}"); return Results.Ok(new { ok = true, id = m.Id, message = "会议已创建" }); }).RequireAuthorization(); app.MapPut("/api/admin/meetings/{id:int}", async (int id, MeetingReq req, ClaimsPrincipal p, RmsysDbContext db) => { var m = await db.Meetings.FirstOrDefaultAsync(x => x.Id == id); if (m == null) return Results.Ok(new { ok = false, message = "会议不存在" }); // 状态守卫:结果已登记 / 已归档 的会议不允许再改基本信息 var denyEdit = Auth.MeetingGuard.CheckWritable(m); if (denyEdit != null) return Results.Json(new { ok = false, message = denyEdit }, statusCode: 409); if (!string.IsNullOrWhiteSpace(req.name)) { var newName = req.name!.Trim(); if (newName != m.Name && await db.Meetings.AnyAsync(x => x.IsDeleted == 0 && x.Name == newName && x.Id != id)) return Results.Ok(new { ok = false, message = "会议名称已存在" }); m.Name = newName; } if (req.sessionNo.HasValue) m.SessionNo = req.sessionNo; if (req.heldAt.HasValue) m.HeldAt = req.heldAt; if (req.place != null) m.Place = req.place; if (req.host != null) m.Host = req.host; if (!string.IsNullOrWhiteSpace(req.secret)) { m.Secret = req.secret!; m.SecretStars = StarsFor(req.secret); } if (!string.IsNullOrWhiteSpace(req.pwdLevel)) m.PwdLevel = req.pwdLevel!; if (!string.IsNullOrWhiteSpace(req.type) && MeetingType.All.Contains(req.type)) m.Type = req.type!; // P1-3 状态只能走「相邻推进」规则,不能借编辑直接跳到任意状态 if (!string.IsNullOrWhiteSpace(req.status) && req.status != m.Status) { if (!MeetingStatus.All.Contains(req.status!)) return Results.Ok(new { ok = false, message = "状态非法" }); if (!Auth.MeetingGuard.CanTransition(m.Status, req.status!)) return Results.Json(new { ok = false, message = Auth.MeetingGuard.TransitionDenied(m.Status, req.status!), allowedNext = Auth.MeetingGuard.AllowedNext(m.Status) }, statusCode: 409); var oldSt = m.Status; m.Status = req.status!; await Audit(db, p, "变更会议状态", m.Name, $"{MeetingStatus.Label(oldSt)} → {MeetingStatus.Label(m.Status)}(随编辑保存)"); } m.UpdatedAt = DateTime.Now; await db.SaveChangesAsync(); await Audit(db, p, "编辑会议", m.Name, null); return Results.Ok(new { ok = true, message = "会议已保存" }); }).RequireAuthorization(); // 软删除:进回收站,不做物理删除(政务系统需可追溯) app.MapDelete("/api/admin/meetings/{id:int}", async (int id, ClaimsPrincipal p, RmsysDbContext db) => { var m = await db.Meetings.FirstOrDefaultAsync(x => x.Id == id && x.IsDeleted == 0); if (m == null) return Results.Ok(new { ok = false, message = "会议不存在或已删除" }); if (MeetingStatus.Held.Contains(m.Status)) return Results.Ok(new { ok = false, message = "已上会的会议不允许删除,请先归档确认" }); m.IsDeleted = 1; m.UpdatedAt = DateTime.Now; await db.SaveChangesAsync(); await Audit(db, p, "删除会议", m.Name, "已移入回收站"); return Results.Ok(new { ok = true, message = "已删除(可在回收站恢复,操作已留痕)" }); }).RequireAuthorization(); app.MapPut("/api/admin/meetings/{id:int}/status", async (int id, StatusReq req, ClaimsPrincipal p, RmsysDbContext db) => { var m = await db.Meetings.FirstOrDefaultAsync(x => x.Id == id); if (m == null) return Results.Ok(new { ok = false, message = "会议不存在" }); var allowed = MeetingStatus.All; if (!allowed.Contains(req.status ?? "")) return Results.Ok(new { ok = false, message = "状态非法" }); // 状态机:只允许相邻推进/回退,不能从「已归档」直接跳回「筹备中」 if (!Auth.MeetingGuard.CanTransition(m.Status, req.status!)) return Results.Json(new { ok = false, message = Auth.MeetingGuard.TransitionDenied(m.Status, req.status!), allowedNext = Auth.MeetingGuard.AllowedNext(m.Status) }, statusCode: 409); var old = m.Status; m.Status = req.status!; m.UpdatedAt = DateTime.Now; await db.SaveChangesAsync(); await Audit(db, p, "变更会议状态", m.Name, $"{MeetingStatus.Label(old)} → {MeetingStatus.Label(m.Status)}"); return Results.Ok(new { ok = true, status = m.Status, statusLabel = MeetingStatus.Label(m.Status) }); }).RequireAuthorization(); // ============ P1-4 撤回归档(已归档 → 结果已登记,只放这一条逆向出口)============ app.MapPost("/api/admin/meetings/{id:int}/unarchive", async (int id, ClaimsPrincipal p, RmsysDbContext db) => { var m = await db.Meetings.FirstOrDefaultAsync(x => x.Id == id && x.IsDeleted == 0); if (m == null) return Results.Ok(new { ok = false, message = "会议不存在" }); if (m.Status != MeetingStatus.Archived) return Results.Ok(new { ok = false, message = $"只有「已归档」的会议需要撤回归档,当前是「{MeetingStatus.Label(m.Status)}」" }); m.Status = MeetingStatus.Registered; m.UpdatedAt = DateTime.Now; await db.SaveChangesAsync(); await Audit(db, p, "撤回归档", m.Name, "已归档 → 结果已登记"); return Results.Ok(new { ok = true, status = m.Status, statusLabel = MeetingStatus.Label(m.Status), message = "已撤回归档" }); }).RequireAuthorization(); // ============ 批注(评审意见 P0:原先只能看不能写)============ app.MapPost("/api/admin/cadres/{cadreId:int}/notes", async ( int cadreId, NoteReq req, ClaimsPrincipal p, RmsysDbContext db) => { if (string.IsNullOrWhiteSpace(req.content)) return Results.Ok(new { ok = false, message = "批注内容不能为空" }); var cadre = await db.Cadres.FirstOrDefaultAsync(x => x.Id == cadreId); if (cadre == null) return Results.Ok(new { ok = false, message = "干部不存在" }); // 状态守卫:结果已登记/已归档的会议不允许再写批注 var denyNote = await MeetingWriteDeny(db, req.meetingId); if (denyNote != null) return Results.Json(new { ok = false, message = denyNote }, statusCode: 409); var n = new CadreNote { CadreId = cadreId, MeetingId = req.meetingId, FieldName = req.fieldName, Content = req.content!.Trim(), Source = req.source ?? "admin", Author = p.FindFirst(ClaimTypes.Name)?.Value ?? "—", CreatedAt = DateTime.Now }; db.CadreNotes.Add(n); await db.SaveChangesAsync(); await Audit(db, p, "新增批注", cadre.Name, n.Content); return Results.Ok(new { ok = true, id = n.Id, author = n.Author, at = n.CreatedAt.ToString("yyyy-MM-dd HH:mm"), message = "批注已保存" }); }).RequireAuthorization(); app.MapGet("/api/admin/cadres/{cadreId:int}/notes", async (int cadreId, RmsysDbContext db) => { var list = await db.CadreNotes.Where(x => x.CadreId == cadreId) .OrderBy(x => x.Id) .Select(x => new { x.Id, x.FieldName, x.Content, x.Source, x.Author, at = x.CreatedAt.ToString("yyyy-MM-dd HH:mm") }) .ToListAsync(); return Results.Ok(list); }).RequireAuthorization(); app.MapDelete("/api/admin/notes/{id:int}", async (int id, ClaimsPrincipal p, RmsysDbContext db) => { var n = await db.CadreNotes.FirstOrDefaultAsync(x => x.Id == id); if (n == null) return Results.Ok(new { ok = false, message = "批注不存在" }); var denyDelNote = await MeetingWriteDeny(db, n.MeetingId); if (denyDelNote != null) return Results.Json(new { ok = false, message = denyDelNote }, statusCode: 409); db.CadreNotes.Remove(n); await db.SaveChangesAsync(); await Audit(db, p, "删除批注", "cadre#" + n.CadreId, n.Content); return Results.Ok(new { ok = true, message = "批注已删除" }); }).RequireAuthorization(); // ============ 上会结果 / 表决(评审意见:决定类会议要留痕)============ app.MapPut("/api/admin/meetings/{meetingId:int}/cadres/{cadreId:int}/result", async ( int meetingId, int cadreId, ResultReq req, ClaimsPrincipal p, RmsysDbContext db) => { var mc = await db.MeetingCadres.FirstOrDefaultAsync(x => x.MeetingId == meetingId && x.CadreId == cadreId); if (mc == null) return Results.Ok(new { ok = false, message = "该干部不在本次上会名单内" }); // 状态守卫:结果已登记/已归档的会议不允许再改表决结果 var denyResult = await MeetingWriteDeny(db, meetingId); if (denyResult != null) return Results.Json(new { ok = false, message = denyResult }, statusCode: 409); var results = new[] { CadreResult.Pending, CadreResult.Passed, CadreResult.Deferred, CadreResult.Rejected, CadreResult.Withdrawn }; if (!results.Contains(req.result ?? "")) return Results.Ok(new { ok = false, message = "结果取值非法" }); mc.Result = req.result!; mc.Vote = req.vote; mc.Remark = req.remark; mc.DecidedAt = req.result == CadreResult.Pending ? null : DateTime.Now; await db.SaveChangesAsync(); var cadre = await db.Cadres.Where(x => x.Id == cadreId).Select(x => x.Name).FirstAsync(); await Audit(db, p, "登记上会结果", cadre, $"结果={mc.Result}" + (mc.Vote != null ? $" 表决={mc.Vote}" : "")); return Results.Ok(new { ok = true, result = mc.Result, vote = mc.Vote }); }).RequireAuthorization(); // ============ P1-9 上会顺序保存(拖动排序后写 MeetingCadres.SortNo)============ app.MapPut("/api/admin/meetings/{id:int}/cadre-order", async ( int id, CadreOrderReq req, ClaimsPrincipal p, RmsysDbContext db) => { var m = await db.Meetings.FirstOrDefaultAsync(x => x.Id == id && 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 ids = req.cadreIds ?? new List(); if (ids.Count == 0) return Results.Ok(new { ok = false, message = "顺序为空" }); var mcs = await db.MeetingCadres.Where(x => x.MeetingId == id).ToListAsync(); if (mcs.Count != ids.Count) return Results.Ok(new { ok = false, message = $"名单人数不一致(库内 {mcs.Count} 人,提交 {ids.Count} 人),请刷新后重试" }); if (ids.Distinct().Count() != ids.Count) return Results.Ok(new { ok = false, message = "提交的顺序里有重复人员" }); for (var i = 0; i < ids.Count; i++) { var mc = mcs.FirstOrDefault(x => x.CadreId == ids[i]); if (mc == null) return Results.Ok(new { ok = false, message = "提交的干部不在本场会议名单内" }); mc.SortNo = i + 1; } m.UpdatedAt = DateTime.Now; await db.SaveChangesAsync(); await Audit(db, p, "调整上会顺序", m.Name, $"{ids.Count} 人"); return Results.Ok(new { ok = true, message = "上会顺序已保存" }); }).RequireAuthorization(); // ============ P1-6 合并同名档案(把 id 并入 intoId,然后删除 id)============ app.MapPost("/api/admin/cadres/{id:int}/merge", async ( int id, MergeReq req, ClaimsPrincipal p, RmsysDbContext db) => { if (req.intoId == null) return Results.Ok(new { ok = false, message = "请选择要保留的档案" }); var intoId = req.intoId.Value; if (intoId == id) return Results.Ok(new { ok = false, message = "保留档案与被合并档案不能是同一条" }); var src = await db.Cadres.FirstOrDefaultAsync(x => x.Id == id); var dst = await db.Cadres.FirstOrDefaultAsync(x => x.Id == intoId); if (src == null || dst == null) return Results.Ok(new { ok = false, message = "档案不存在" }); if (src.Name.Trim() != dst.Name.Trim()) return Results.Ok(new { ok = false, message = "只有姓名相同的档案可以合并" }); // 1) 会议名单:同一场会里两边都在,就删掉重复行;否则改挂到保留档案 var srcMc = await db.MeetingCadres.Where(x => x.CadreId == id).ToListAsync(); var movedMc = 0; var droppedMc = 0; foreach (var mc in srcMc) { var dup = await db.MeetingCadres.FirstOrDefaultAsync(x => x.MeetingId == mc.MeetingId && x.CadreId == dst.Id); if (dup != null) { db.MeetingCadres.Remove(mc); droppedMc++; } else { mc.CadreId = dst.Id; movedMc++; } } // 2) 其余挂靠数据一律改挂 var moved = new Dictionary(); // 简历 / 家庭成员 / 其他关系:内容重复的不再搬(否则合并后会出重复条目) var dstResumes = await db.CadreResumes.Where(x => x.CadreId == dst.Id).Select(x => x.Content).ToListAsync(); foreach (var x in await db.CadreResumes.Where(x => x.CadreId == id).ToListAsync()) { if (dstResumes.Contains(x.Content)) { db.CadreResumes.Remove(x); continue; } x.CadreId = dst.Id; dstResumes.Add(x.Content); moved["简历"] = moved.GetValueOrDefault("简历") + 1; } var dstFam = await db.CadreFamilies.Where(x => x.CadreId == dst.Id).Select(x => x.Relation + "|" + x.Name).ToListAsync(); foreach (var x in await db.CadreFamilies.Where(x => x.CadreId == id).ToListAsync()) { var key = x.Relation + "|" + x.Name; if (dstFam.Contains(key)) { db.CadreFamilies.Remove(x); continue; } x.CadreId = dst.Id; dstFam.Add(key); moved["家庭成员"] = moved.GetValueOrDefault("家庭成员") + 1; } var dstRel = await db.CadreRelations.Where(x => x.CadreId == dst.Id).Select(x => x.Relation + "|" + x.RelatedName).ToListAsync(); foreach (var x in await db.CadreRelations.Where(x => x.CadreId == id).ToListAsync()) { var key = x.Relation + "|" + x.RelatedName; if (dstRel.Contains(key)) { db.CadreRelations.Remove(x); continue; } x.CadreId = dst.Id; dstRel.Add(key); moved["其他关系"] = moved.GetValueOrDefault("其他关系") + 1; } foreach (var x in await db.CadreNotes.Where(x => x.CadreId == id).ToListAsync()) { x.CadreId = dst.Id; moved["批注"] = moved.GetValueOrDefault("批注") + 1; } foreach (var x in await db.ReadReceipts.Where(x => x.CadreId == id).ToListAsync()) { x.CadreId = dst.Id; moved["已阅回执"] = moved.GetValueOrDefault("已阅回执") + 1; } foreach (var x in await db.Materials.Where(x => x.CadreId == id).ToListAsync()) { x.CadreId = dst.Id; moved["材料"] = moved.GetValueOrDefault("材料") + 1; } foreach (var x in await db.AppointDocCadres.Where(x => x.CadreId == id).ToListAsync()) { x.CadreId = dst.Id; moved["任免文件关联"] = moved.GetValueOrDefault("任免文件关联") + 1; } // 3) 冲突字段按提交值覆盖(出生年月等) if (!string.IsNullOrWhiteSpace(req.birth)) dst.Birth = req.birth!; if (!string.IsNullOrWhiteSpace(req.nation) && string.IsNullOrWhiteSpace(dst.Nation)) dst.Nation = req.nation; if (dst.DeptId == null && src.DeptId != null) dst.DeptId = src.DeptId; dst.UpdatedAt = DateTime.Now; db.Cadres.Remove(src); await db.SaveChangesAsync(); var detail = string.Join("、", moved.Select(kv => kv.Key + kv.Value)) + (movedMc > 0 ? $"、名单改挂{movedMc}" : "") + (droppedMc > 0 ? $"、名单去重{droppedMc}" : ""); await Audit(db, p, "合并档案", dst.Name, $"#{id}[出生{src.Birth ?? "-"}] 并入 #{intoId}[出生{dst.Birth ?? "-"}]({detail})"); return Results.Ok(new { ok = true, keepId = intoId, message = "档案已合并", detail = detail }); }).RequireAuthorization(); // ============ 任免文件(软删除,文件保留可恢复)============ app.MapDelete("/api/admin/docs/{id:int}", async (int id, ClaimsPrincipal p, RmsysDbContext db) => { var d = await db.AppointDocs.FirstOrDefaultAsync(x => x.Id == id && x.IsDeleted == 0); if (d == null) return Results.Ok(new { ok = false, message = "任免文件不存在或已删除" }); d.IsDeleted = 1; await db.SaveChangesAsync(); await Audit(db, p, "删除任免文件", d.Title ?? ("文稿 " + d.Id), $"文号={d.DocNo} 处室={d.DeptId}"); return Results.Ok(new { ok = true, message = "任免文件已删除(可在回收站恢复)" }); }).RequireAuthorization(); // ============ 处室 ============ app.MapPost("/api/admin/depts", async (DeptReq req, ClaimsPrincipal p, RmsysDbContext db) => { var name = (req.name ?? "").Trim(); if (string.IsNullOrWhiteSpace(name)) return Results.Ok(new { ok = false, message = "处室名称不能为空" }); if (await db.Depts.AnyAsync(x => x.Name == name)) return Results.Ok(new { ok = false, message = "处室名称已存在" }); // 编码不对外暴露:未传就自动生成(前端只填名称/职责/负责人) var code = (req.code ?? "").Trim(); if (string.IsNullOrWhiteSpace(code)) { code = "dept" + DateTime.Now.ToString("yyMMddHHmmss"); while (await db.Depts.AnyAsync(x => x.Code == code)) code = "dept" + DateTime.Now.AddSeconds(1).ToString("yyMMddHHmmss"); } if (await db.Depts.AnyAsync(x => x.Code == code)) return Results.Ok(new { ok = false, message = "处室编码已存在" }); var d = new Dept { Code = code, Name = name, Duty = req.duty?.Trim(), Leader = req.leader?.Trim(), Sort = req.sort ?? 99, Enabled = req.enabled ?? 1, CreatedAt = DateTime.Now }; db.Depts.Add(d); await db.SaveChangesAsync(); await Audit(db, p, "新增处室", d.Name, d.Code); return Results.Ok(new { ok = true, id = d.Id, message = "处室已创建" }); }).RequireAuthorization(); app.MapPut("/api/admin/depts/{id:int}", async (int id, DeptReq req, ClaimsPrincipal p, RmsysDbContext db) => { var d = await db.Depts.FirstOrDefaultAsync(x => x.Id == id); if (d == null) return Results.Ok(new { ok = false, message = "处室不存在" }); if (!string.IsNullOrWhiteSpace(req.name)) { var nm = req.name!.Trim(); if (await db.Depts.AnyAsync(x => x.Name == nm && x.Id != id)) return Results.Ok(new { ok = false, message = "处室名称已存在" }); d.Name = nm; } if (req.duty != null) d.Duty = req.duty; if (req.leader != null) d.Leader = req.leader; if (req.sort.HasValue) d.Sort = req.sort.Value; if (req.enabled.HasValue) d.Enabled = req.enabled.Value; await db.SaveChangesAsync(); await Audit(db, p, "编辑处室", d.Name, null); return Results.Ok(new { ok = true, message = "处室已保存" }); }).RequireAuthorization(); app.MapDelete("/api/admin/depts/{id:int}", async (int id, ClaimsPrincipal p, RmsysDbContext db) => { var d = await db.Depts.FirstOrDefaultAsync(x => x.Id == id); if (d == null) return Results.Ok(new { ok = false, message = "处室不存在" }); var used = await db.Cadres.CountAsync(x => x.DeptId == id); if (used > 0) return Results.Ok(new { ok = false, message = $"该处室下还有 {used} 名在册干部,不能删除;不再使用请改为「停用」" }); db.Depts.Remove(d); await db.SaveChangesAsync(); await Audit(db, p, "删除处室", d.Name, d.Code); return Results.Ok(new { ok = true, message = "处室已删除" }); }).RequireAuthorization(); // ============ PAD 端数据设置(保存后下次导出数据包即生效)============ app.MapPut("/api/admin/packages/{meetingId:int}/settings", async ( int meetingId, PadSetReq req, ClaimsPrincipal p, RmsysDbContext db) => { var pkg = await db.DataPackages.Where(x => x.MeetingId == meetingId) .OrderByDescending(x => x.Id).FirstOrDefaultAsync(); // 状态守卫:结果已登记 / 已归档的会议不允许再改 PAD 端设置 var denySet = await MeetingWriteDeny(db, meetingId); if (denySet != null) return Results.Json(new { ok = false, message = denySet }, statusCode: 409); if (pkg == null) { // 尚无数据包:先建一个草稿,参数一并存下 pkg = new DataPackage { MeetingId = meetingId, PkgNo = "PK" + DateTime.Now.ToString("yyyyMMdd-HHmmss"), CreatedAt = DateTime.Now, CreatedBy = p.FindFirst(ClaimTypes.Name)?.Value }; db.DataPackages.Add(pkg); } if (req.expireAt.HasValue) pkg.ExpireAt = req.expireAt; if (!string.IsNullOrWhiteSpace(req.watermark)) pkg.Watermark = req.watermark; if (!string.IsNullOrWhiteSpace(req.fontLevel)) pkg.FontLevel = req.fontLevel; if (req.allowFontChange.HasValue) pkg.AllowFontChange = req.allowFontChange.Value; if (!string.IsNullOrWhiteSpace(req.clearPolicy)) pkg.ClearPolicy = req.clearPolicy; if (req.switchesJson != null) pkg.SwitchesJson = req.switchesJson; await db.SaveChangesAsync(); if (req.matOrder != null && req.matOrder.Count > 0) { var olds = await db.PackageItems.Where(x => x.PackageId == pkg.Id).ToListAsync(); db.PackageItems.RemoveRange(olds); for (var i = 0; i < req.matOrder.Count; i++) { db.PackageItems.Add(new PackageItem { PackageId = pkg.Id, MaterialType = req.matOrder[i], Enabled = 1, Sort = i + 1 }); } await db.SaveChangesAsync(); } await Audit(db, p, "保存PAD端数据设置", pkg.PkgNo, $"有效期={pkg.ExpireAt:yyyy-MM-dd HH:mm} 字号={pkg.FontLevel}"); return Results.Ok(new { ok = true, pkgNo = pkg.PkgNo, message = "设置已保存,重新导出数据包后生效" }); }).RequireAuthorization(); } /// /// 会议内容写操作的状态守卫:返回 null 表示可写,否则返回拒绝原因。 /// 会议 id 为空(例如全局批注)时不做限制。 /// private static async Task MeetingWriteDeny(RmsysDbContext db, int? meetingId) { if (!meetingId.HasValue) return null; var m = await db.Meetings.AsNoTracking().FirstOrDefaultAsync(x => x.Id == meetingId.Value); if (m == null) return "会议不存在"; return Auth.MeetingGuard.CheckWritable(m); } // ---------------- 审计 ---------------- private static async Task Audit(RmsysDbContext db, ClaimsPrincipal p, string action, string target, string? detail) { db.AuditLogs.Add(new AuditLog { At = DateTime.Now, UserId = int.TryParse(p.FindFirst("uid")?.Value, out var uid) ? uid : null, UserName = p.FindFirst(ClaimTypes.Name)?.Value, Role = p.FindFirst(ClaimTypes.Role)?.Value, Action = action, Target = target, Detail = detail, Device = "API" }); await db.SaveChangesAsync(); } // ---------------- 请求体 ---------------- public record MeetingReq(string? type, int? sessionNo, string? name, DateTime? heldAt, string? place, string? host, string? secret, string? pwdLevel, string? batchNo, string? status); public record StatusReq(string? status); /// 密级 → 标志星级(国标:秘密★ 机密★★ 绝密★★★;公开/内部不作密级标志) private static string StarsFor(string? secret) => (secret ?? "").Trim() switch { "秘密" => "★", "机密" => "★★", "绝密" => "★★★", _ => "" // 公开、内部:不显示星级 }; public record NoteReq(int? meetingId, string? fieldName, string? content, string? source); public record ResultReq(string? result, string? vote, string? remark); public record DeptReq(string? code, string? name, string? duty, string? leader, int? sort, int? enabled); public record CadreOrderReq(List? cadreIds); public record MergeReq(int? intoId, string? birth, string? nation); public record PadSetReq(DateTime? expireAt, string? watermark, string? fontLevel, int? allowFontChange, string? clearPolicy, string? switchesJson, List? matOrder); }