Add aggregate voter registration management

This commit is contained in:
biss committed 2026-09-08 08:36:05 +08:00
1 parent 6d765ac8f3
commit e96a56314f
13 files changed
+1334 -7

No files matched your search

@@ -15,7 +15,7 @@
{
<div class="admin-stat-grid">
<article><span>可管理地区</span><strong>@regionCount</strong><small>含下级辖区</small></article>
<article><span>有效选民</span><strong>@activeVoterCount</strong><small>可参加后续资格核验</small></article>
<article><span>已登记选民人数</span><strong>@registeredVoterCount</strong><small>具名有效选民 @activeVoterCount;汇总数不用于线上资格核验</small></article>
<article><span>候选人资料</span><strong>@candidateCount</strong><small>含待审核与已公示</small></article>
<article><span>启用计票所</span><strong>@stationCount</strong><small>须单独分配工作人员</small></article>
</div>
@@ -24,10 +24,11 @@
</section>
@code {
private bool loaded; private string? error; private int regionCount, activeVoterCount, candidateCount, stationCount;
private bool loaded; private string? error; private int regionCount, activeVoterCount, aggregateVoterCount, candidateCount, stationCount;
private int registeredVoterCount => activeVoterCount + aggregateVoterCount;
protected override async Task OnInitializedAsync()
{
try { var userId = RegionScopeService.GetRequiredUserId((await AuthenticationStateTask).User); var scope = await Scope.GetManagedRegionIdsAsync(userId, CancellationToken.None); regionCount = scope.Count; activeVoterCount = await Db.Voters.CountAsync(x => scope.Contains(x.RegionId) && x.Status == VoterStatus.Active); candidateCount = await Db.Candidates.CountAsync(x => scope.Contains(x.RegionId)); stationCount = await Db.CountingStations.CountAsync(x => scope.Contains(x.RegionId) && x.IsActive); loaded = true; }
try { var userId = RegionScopeService.GetRequiredUserId((await AuthenticationStateTask).User); var scope = await Scope.GetManagedRegionIdsAsync(userId, CancellationToken.None); regionCount = scope.Count; activeVoterCount = await Db.Voters.CountAsync(x => scope.Contains(x.RegionId) && x.Status == VoterStatus.Active); aggregateVoterCount = await Db.VoterRegistrationAggregates.Where(x => scope.Contains(x.RegionId)).SumAsync(x => (int?)x.RegisteredCount) ?? 0; candidateCount = await Db.Candidates.CountAsync(x => scope.Contains(x.RegionId)); stationCount = await Db.CountingStations.CountAsync(x => scope.Contains(x.RegionId) && x.IsActive); loaded = true; }
catch (Exception) { error = "无法读取管理范围。请确认此账号已完成地域角色分配。"; }
}
[CascadingParameter] private Task<AuthenticationState> AuthenticationStateTask { get; set; } = default!;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore;
namespace ElectionSystem.Api.Controllers;
[Route("api/admin/voters")]
public sealed class VotersController(ElectionDbContext dbContext, Modules.Organization.RegionScopeService scopeService, Modules.Organization.RegionCascadeFilterService cascadeFilterService, VoterImportService importService) : AdministrationControllerBase(scopeService)
public sealed class VotersController(ElectionDbContext dbContext, Modules.Organization.RegionScopeService scopeService, Modules.Organization.RegionCascadeFilterService cascadeFilterService, VoterImportService importService, VoterRegistrationAggregateService aggregateService) : AdministrationControllerBase(scopeService)
{
[HttpGet]
public async Task<ActionResult<PagedResult<object>>> List([FromQuery] Guid? regionId, [FromQuery] VoterStatus? status, [FromQuery] string? keyword, [FromQuery] int page = 1, [FromQuery] int pageSize = 50, CancellationToken cancellationToken = default)
@@ -85,9 +85,33 @@ public sealed class VotersController(ElectionDbContext dbContext, Modules.Organi
[HttpPost("import/{token:guid}/confirm")]
public async Task<ActionResult> ConfirmImport(Guid token, CancellationToken cancellationToken) => Ok(new { importedCount = await importService.ConfirmAsync(ActorId, token, cancellationToken) });
[HttpGet("aggregate-registrations")]
public async Task<ActionResult> ListAggregateRegistrations(CancellationToken cancellationToken)
{
var allowed = await ScopeService.GetManagedRegionIdsAsync(ActorId, cancellationToken);
var rows = await (from aggregate in dbContext.VoterRegistrationAggregates.AsNoTracking()
join region in dbContext.AdministrativeRegions.AsNoTracking() on aggregate.RegionId equals region.Id
where allowed.Contains(aggregate.RegionId)
orderby region.Code
select new { aggregate.RegionId, RegionCode = region.Code, RegionName = region.Name, aggregate.RegisteredCount, aggregate.Reason, aggregate.UpdatedAtUtc }).ToListAsync(cancellationToken);
return Ok(rows);
}
[HttpPut("aggregate-registrations/{regionId:guid}")]
public async Task<ActionResult> SetAggregateRegistration(Guid regionId, SetVoterRegistrationAggregateRequest request, CancellationToken cancellationToken)
{
try
{
var aggregate = await aggregateService.SetAsync(ActorId, regionId, request.RegisteredCount, request.Reason, cancellationToken);
return Ok(new { aggregate.RegionId, aggregate.RegisteredCount, aggregate.UpdatedAtUtc });
}
catch (ArgumentException exception) { return ValidationProblem(exception.Message); }
}
}
public sealed record CreateVoterRequest(string VoterNumber, string DisplayName, Guid RegionId);
public sealed record SetVoterStatusRequest(VoterStatus Status, string? Reason);
public sealed record SetVoterRegistrationAggregateRequest(int RegisteredCount, string? Reason);
public sealed record VoterCursor(string DisplayName, Guid Id);
internal sealed record VoterCursorRow(Guid Id, string VoterNumber, string DisplayName, Guid RegionId, VoterStatus Status, string? DisabledReason, DateTime UpdatedAtUtc);
@@ -19,6 +19,7 @@ public sealed class ElectionDbContext(DbContextOptions<ElectionDbContext> option
public DbSet<CountingStation> CountingStations => Set<CountingStation>();
public DbSet<UserCountingStationAssignment> UserCountingStationAssignments => Set<UserCountingStationAssignment>();
public DbSet<VoterProfile> Voters => Set<VoterProfile>();
public DbSet<VoterRegistrationAggregate> VoterRegistrationAggregates => Set<VoterRegistrationAggregate>();
public DbSet<CandidateProfile> Candidates => Set<CandidateProfile>();
public DbSet<ElectionActivity> ElectionActivities => Set<ElectionActivity>();
public DbSet<ElectionContest> ElectionContests => Set<ElectionContest>();
@@ -93,6 +94,14 @@ public sealed class ElectionDbContext(DbContextOptions<ElectionDbContext> option
entity.HasOne<AdministrativeRegion>().WithMany().HasForeignKey(x => x.RegionId).OnDelete(DeleteBehavior.Restrict);
entity.HasOne<ApplicationUser>().WithMany().HasForeignKey(x => x.UserId).OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<VoterRegistrationAggregate>(entity =>
{
entity.ToTable("voter_registration_aggregates");
entity.Property(x => x.Reason).HasMaxLength(500).IsRequired();
entity.HasIndex(x => x.RegionId).IsUnique();
entity.HasOne<AdministrativeRegion>().WithMany().HasForeignKey(x => x.RegionId).OnDelete(DeleteBehavior.Restrict);
entity.HasOne<ApplicationUser>().WithMany().HasForeignKey(x => x.SetByUserId).OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<CandidateProfile>(entity =>
{
entity.ToTable("candidate_profiles");
Loaded 3 of 13 files, more files were not shown because too many files have changed in this diff. Show more