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