(
await db.ReadReceipts.AsNoTracking()
.Select(x => x.MeetingId + "|" + x.CadreId + "|" + x.MaterialType + "|" + x.PadNo)
.ToListAsync());
int added = 0, dup = 0, bad = 0;
using var sr = new StreamReader(file.OpenReadStream(), System.Text.Encoding.UTF8);
var text = await sr.ReadToEndAsync();
foreach (var raw in text.Split('\n'))
{
var line = raw.Trim();
if (line.Length == 0) continue;
try
{
using var doc = System.Text.Json.JsonDocument.Parse(line);
var r = doc.RootElement;
var mid = r.GetProperty("meetingId").GetInt32();
var cid = r.GetProperty("cadreId").GetInt32();
var type = r.GetProperty("type").GetString() ?? "";
var pad = r.TryGetProperty("padNo", out var pn) ? (pn.GetString() ?? "") : "";
var at = r.TryGetProperty("readAt", out var ra) && DateTime.TryParse(ra.GetString(), out var dt)
? dt : DateTime.Now;
if (mid <= 0 || cid <= 0 || type.Length == 0 || pad.Length == 0) { bad++; continue; }
var key = mid + "|" + cid + "|" + type + "|" + pad;
if (!seen.Add(key)) { dup++; continue; }
db.ReadReceipts.Add(new ReadReceipt
{
MeetingId = mid, CadreId = cid, MaterialType = type, PadNo = pad,
ReadAt = at, ImportedAt = DateTime.Now, ImportedBy = user
});
added++;
}
catch { bad++; }
}
await db.SaveChangesAsync();
await Audit(db, p, "导入已阅回执", file.FileName,
$"新增 {added} 条,重复 {dup} 条,无效 {bad} 条");
return Results.Ok(new
{
ok = true,
added, dup, bad,
message = $"导入完成:新增 {added} 条,重复 {dup} 条,无效 {bad} 条"
});
}).RequireAuthorization()
.NoAntiforgery(); // 纯 JWT 鉴权(Bearer 头不被浏览器自动携带),CSRF 不成立;
// .NET 8 对 IFormFile 端点默认要求抗伪造令牌,这里显式关掉(见 Compat/NetCompat.cs)
app.MapGet("/api/admin/receipts", async (int? meetingId, RmsysDbContext db) =>
{
var q = db.ReadReceipts.AsNoTracking();
if (meetingId.HasValue) q = q.Where(x => x.MeetingId == meetingId.Value);
var list = await q.OrderByDescending(x => x.ReadAt).Take(500)
.Select(x => new
{
x.MeetingId, x.CadreId, x.MaterialType, x.PadNo,
readAt = x.ReadAt.ToString("yyyy-MM-dd HH:mm"),
x.ImportedBy
}).ToListAsync();
return Results.Ok(new { ok = true, total = list.Count, list });
}).RequireAuthorization();
}
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();
}
///
/// 预览失败时的提示页。iframe 里直接显示成一行说明,
/// 比返回 404/空响应让用户面对一片空白要好排查。
///
private static IResult PreviewMsg(string msg)
=> Results.Content(
""
+ ""
+ System.Net.WebUtility.HtmlEncode(msg)
+ "
",
"text/html; charset=utf-8");
}
/// 设置任免文件涉及人员
public record CadreIdsReq(List? cadreIds);