using System.Security.Claims;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Rmsys.Domain;
using Rmsys.Domain.Entities;
using Rmsys.Infrastructure;
namespace Rmsys.Api.Endpoints;
///
/// 后台管理端数据接口。
/// /api/admin/bootstrap 输出的字段名与形状和现有 demo(admin.html 里的
/// MEETINGS / DEPTS / CADRES / DOCS / PKGS / PADS / USERS / LOGINS / LOGS)
/// 完全一致,目的是让前端渲染代码一行不改就能接上真库。
///
public static class AdminEndpoints
{
private const string DateFmt = "yyyy-MM-dd HH:mm";
private const string TimeFmt = "yyyy-MM-dd HH:mm:ss";
public static void MapAdminEndpoints(this WebApplication app)
{
// ---------- 导出数据包(生成 SQLite 并下载)----------
// 这是「后台配置真正下发到 PAD」的唯一通道:水印/字号/有效期/设备绑定/清除策略都写在包内 pkg_config
app.MapGet("/api/admin/packages/{meetingId:int}/export", async (
int meetingId, ClaimsPrincipal p, RmsysDbContext db, IWebHostEnvironment env) =>
{
try
{
// 数据包=材料离库:仅「全部处室」且非只读账号可导出
var sc = await Auth.ScopeGuard.ResolveAsync(p, db);
if (!sc.IsAll)
return Results.Json(new { ok = false, message = "「本处室」账号不能导出数据包(包内含全部上会干部材料)" }, statusCode: 403);
if (!sc.CanWrite)
return Auth.ScopeGuard.ReadOnlyDenied();
var dir = Path.Combine(env.ContentRootPath, "App_Data", "packages");
Directory.CreateDirectory(dir);
var name = $"PK{DateTime.Now:yyyyMMdd-HHmmss}.pkg";
var path = Path.Combine(dir, name);
// 加密导出:SM4-CBC 加密 + HMAC-SM3 完整性校验
var r = await new PackageExporter(db)
{
PhotoRoot = Path.Combine(env.ContentRootPath, "App_Data") // 证件照随包下发
}.ExportEncryptedAsync(meetingId, path, "admin");
// 回写数据包状态,便于后台列表展示
var pkg = await db.DataPackages.Where(x => x.MeetingId == meetingId)
.OrderByDescending(x => x.Id).FirstOrDefaultAsync();
if (pkg != null)
{
pkg.Status = PackageStatus.Delivered;
pkg.FilePath = "App_Data/packages/" + name;
pkg.FileSize = r.Bytes;
pkg.DownloadedAt = DateTime.Now;
await db.SaveChangesAsync();
}
// 数据包导出=材料离库,必须留痕
await AuditRead(db, p, "导出数据包", pkg?.PkgNo ?? ("会议#" + meetingId),
$"{name}({r.Bytes / 1024} KB,{r.Cadres} 人,国密SM4加密)");
PruneOldExports(dir, keep: 3);
return Results.File(path, "application/octet-stream", name);
}
catch (Exception e)
{
return Results.Problem("导出失败:" + e.Message);
}
}).RequireAuthorization();
// ---------- 只校验不下载 ----------
app.MapGet("/api/admin/packages/{meetingId:int}/check", async (
int meetingId, ClaimsPrincipal p, RmsysDbContext db, IWebHostEnvironment env) =>
{
var dir = Path.Combine(env.ContentRootPath, "App_Data", "packages");
Directory.CreateDirectory(dir);
var path = Path.Combine(dir, $"check-{meetingId}-{DateTime.Now:HHmmss}.db");
var r = await new PackageExporter(db).ExportAsync(meetingId, path, "check");
var v = PackageVerifier.Verify(path);
try { File.Delete(path); } catch { }
await AuditRead(db, p, "校验数据包", v.PkgNo ?? ("会议#" + meetingId),
v.Ok ? "校验通过" : ("校验未通过:" + string.Join(";", v.Errors.Take(3))));
return Results.Ok(new
{
ok = v.Ok,
pkgNo = v.PkgNo,
meeting = v.MeetingName,
expireAt = v.ExpireAt,
watermark = v.Watermark,
matOrder = v.MatOrder,
counts = new { cadres = v.Cadres, resumes = v.Resumes, families = v.Families, notes = v.Notes, materials = v.Materials, docs = v.Docs, padBound = v.PadBound },
missingTables = v.MissingTables,
missingConfig = v.MissingConfig,
errors = v.Errors,
bytes = v.Bytes
});
}).RequireAuthorization();
app.MapGet("/api/admin/bootstrap", async (int? meetingId, ClaimsPrincipal p, RmsysDbContext db) =>
{
var depts = await db.Depts.OrderBy(x => x.Sort).ToListAsync();
var meetings = await db.Meetings.Where(x => x.IsDeleted == 0).OrderBy(x => x.Id).ToListAsync();
var cur = meetings.FirstOrDefault(x => x.Id == (meetingId ?? meetings.FirstOrDefault()?.Id));
if (cur == null) return Results.Ok(new { error = "尚无会议数据" });
// 查看留痕:谁在什么时候看了哪场会的全套材料
await AuditRead(db, p, "查看会议数据", cur.Name, "后台管理端加载");
var deptCode = depts.ToDictionary(x => x.Id, x => x.Code);
var deptName = depts.ToDictionary(x => x.Id, x => x.Name);
var hasDept = cur.Type == MeetingType.BuWu;
// ---------- 会议 × 干部 ----------
var mcs = await db.MeetingCadres.Where(x => x.MeetingId == cur.Id)
.OrderBy(x => x.SortNo).ToListAsync();
var cadreIds = mcs.Select(x => x.CadreId).ToList();
var cadres = await db.Cadres.Where(x => cadreIds.Contains(x.Id)).ToListAsync();
var mats = await db.Materials.Where(x => x.MeetingId == cur.Id).ToListAsync();
var notes = await db.CadreNotes.Where(x => x.MeetingId == cur.Id || x.MeetingId == null)
.GroupBy(x => x.CadreId)
.Select(g => new { CadreId = g.Key, N = g.Count() })
.ToListAsync();
var noteMap = notes.ToDictionary(x => x.CadreId, x => x.N);
// 已阅回执(PAD 回收后导入的条数,按干部汇总)
var readMap = await db.ReadReceipts.Where(x => x.MeetingId == cur.Id)
.GroupBy(x => x.CadreId)
.Select(g => new { CadreId = g.Key, N = g.Count() })
.ToDictionaryAsync(x => x.CadreId, x => x.N);
// ---------- 数据权限过滤(本处室账号只看本处室)----------
// 「全部处室」「只读」不限;「本处室」按 sysusers.DeptId 过滤干部及其材料/批注。
var scope = await Auth.ScopeGuard.ResolveAsync(p, db);
if (!scope.IsAll)
{
var mineIds = cadres.Where(x => x.DeptId == scope.DeptId).Select(x => x.Id).ToHashSet();
mcs = mineIds.Count == 0
? mcs.Where(_ => false).ToList()
: mcs.Where(x => mineIds.Contains(x.CadreId)).ToList();
cadres = cadres.Where(x => mineIds.Contains(x.Id)).ToList();
mats = mats.Where(x => x.CadreId.HasValue && mineIds.Contains(x.CadreId.Value)).ToList();
noteMap = notes.Where(x => mineIds.Contains(x.CadreId))
.ToDictionary(x => x.CadreId, x => x.N);
}
var cadreOut = new List