Add aggregate voter registration management
This commit is contained in:
1 parent
6d765ac8f3
commit
e96a56314f
13 files changed
+1334
-7
No files matched your search
@@ -26,7 +26,7 @@ dotnet user-secrets remove "Bootstrap:SystemAdministrator:Password" --project sr
|
||||
|
||||
- `/admin/dashboard`:仅显示当前授权范围内的汇总。
|
||||
- `/admin/regions`:首次启动会自动导入随应用发布的 AreaCity 四级目录;村/社区使用本地确认目录。`/admin/region-import` 仅用于受控版本更新。
|
||||
- `/admin/voters` 与 `/admin/voter-import`:按地域维护选民、预校验并确认 Excel 导入。选民列表使用每页 50 条的游标分页,并支持省→市→区县→镇街四级级联筛选。
|
||||
- `/admin/voters` 与 `/admin/voter-import`:按地域维护选民、预校验并确认 Excel 导入。若身份信息不能进入本系统,`/admin/voter-import` 可按有权辖区登记未具名的补充人数并填写原因;它独立于选民档案,只用于登记总量展示,绝不用于线上资格、已投状态或重复投票判断。与具名名单并存时,汇总数必须只填未具名部分,避免重复统计。选民列表使用每页 50 条的游标分页,并支持省→市→区县→镇街四级级联筛选。
|
||||
- `/admin/candidates`:维护候选人资料和服务端允许的状态转换。
|
||||
- `/admin/roles`:系统管理员授予地域管理员、计票员或监督员角色。
|
||||
- `/admin/counting-stations`:建立辖区计票所,并向已获地域角色的人员分配具体计票所;列表同样使用游标分页及四级级联筛选。
|
||||
|
||||
@@ -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);
|
||||
Loaded 3 of 13 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user