管理端后端

This commit is contained in:
biss committed 2026-08-29 17:26:59 +08:00
1 parent eebc5518c5
commit 4ef6f6c90d
24 files changed
+1745 -10

No files matched your search

@@ -8,12 +8,18 @@ public sealed class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logge
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
var traceId = httpContext.TraceIdentifier;
logger.LogError(exception, "Unhandled request failure. TraceId: {TraceId}", traceId);
var status = exception switch
{
UnauthorizedAccessException => StatusCodes.Status403Forbidden,
InvalidOperationException => StatusCodes.Status409Conflict,
_ => StatusCodes.Status500InternalServerError
};
if (status >= 500) logger.LogError(exception, "Unhandled request failure. TraceId: {TraceId}", traceId);
var problem = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "An unexpected error occurred.",
Type = "https://tools.ietf.org/html/rfc9110#section-15.6.1",
Status = status,
Title = status == StatusCodes.Status403Forbidden ? "The action is outside your authorized scope." : status == StatusCodes.Status409Conflict ? "The request conflicts with current data or workflow state." : "An unexpected error occurred.",
Type = status == StatusCodes.Status403Forbidden ? "https://tools.ietf.org/html/rfc9110#section-15.5.4" : "https://tools.ietf.org/html/rfc9110#section-15.6.1",
Instance = httpContext.Request.Path
};
problem.Extensions["traceId"] = traceId;
@@ -0,0 +1,13 @@
using ElectionSystem.Api.Modules.Organization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ElectionSystem.Api.Controllers;
[ApiController]
[Authorize]
public abstract class AdministrationControllerBase(RegionScopeService scopeService) : ControllerBase
{
protected Guid ActorId => RegionScopeService.GetRequiredUserId(User);
protected RegionScopeService ScopeService { get; } = scopeService;
}
@@ -0,0 +1,45 @@
using ElectionSystem.Api.Infrastructure.Persistence;
using ElectionSystem.Api.Modules.Organization.Domain;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace ElectionSystem.Api.Controllers;
[Route("api/admin/administrative-regions")]
public sealed class AdministrativeRegionsController(ElectionDbContext dbContext, Modules.Organization.RegionScopeService scopeService, Modules.Organization.AreaCityCatalogImportService catalogImportService) : AdministrationControllerBase(scopeService)
{
[HttpGet]
public async Task<ActionResult> List([FromQuery] Guid? parentId, CancellationToken cancellationToken)
{
var allowed = await ScopeService.GetManagedRegionIdsAsync(ActorId, cancellationToken);
var items = await dbContext.AdministrativeRegions.Where(x => x.ParentId == parentId && allowed.Contains(x.Id)).OrderBy(x => x.Code)
.Select(x => new { x.Id, x.ParentId, x.Code, x.Name, x.Level, x.Source, x.SourceVersion, x.IsSynthetic, x.IsActive }).ToListAsync(cancellationToken);
return Ok(items);
}
[HttpPost("area-city-import")]
[RequestSizeLimit(15_000_000)]
public async Task<ActionResult> ImportAreaCity(IFormFile file, [FromForm] string sourceVersion, CancellationToken cancellationToken)
{
await ScopeService.RequireSystemAdministratorAsync(ActorId, cancellationToken);
if (file.Length == 0 || !Path.GetExtension(file.FileName).Equals(".csv", StringComparison.OrdinalIgnoreCase)) return ValidationProblem("请上传 AreaCity 的 ok_data_level4.csv 文件。");
await using var stream = file.OpenReadStream();
return Ok(await catalogImportService.ImportAsync(stream, sourceVersion, cancellationToken));
}
[HttpPost]
public async Task<ActionResult> Create(CreateAdministrativeRegionRequest request, CancellationToken cancellationToken)
{
await ScopeService.RequireSystemAdministratorAsync(ActorId, cancellationToken);
if (request.Level != AdministrativeRegionLevel.Village) return ValidationProblem("基础省市区镇目录必须由受控目录导入,管理端仅可新增经确认的村/社区。");
var parent = await dbContext.AdministrativeRegions.SingleOrDefaultAsync(x => x.Id == request.ParentId && x.IsActive, cancellationToken);
if (parent is null || parent.Level != AdministrativeRegionLevel.Township) return ValidationProblem("村/社区必须属于有效的镇/街道。");
if (await dbContext.AdministrativeRegions.AnyAsync(x => x.Code == request.Code && x.SourceVersion == request.SourceVersion, cancellationToken)) return Conflict(new { code = "region_code_exists" });
var region = new AdministrativeRegion { ParentId = parent.Id, Code = request.Code.Trim(), Name = request.Name.Trim(), Level = request.Level, Source = "LocalApprovedVillageImport", SourceVersion = request.SourceVersion.Trim() };
dbContext.AdministrativeRegions.Add(region);
await dbContext.SaveChangesAsync(cancellationToken);
return CreatedAtAction(nameof(List), new { parentId = parent.Id }, new { region.Id });
}
}
public sealed record CreateAdministrativeRegionRequest(Guid ParentId, string Code, string Name, AdministrativeRegionLevel Level, string SourceVersion);
Loaded 3 of 24 files, more files were not shown because too many files have changed in this diff. Show more