feature: Site & Gateway Registry + Clinical Sync Contracts

This commit is contained in:
voltsrage
2026-06-23 04:31:15 +08:00
parent c9994b1ba2
commit efe5419da4
24 changed files with 547 additions and 19 deletions
@@ -0,0 +1,80 @@
using Microsoft.EntityFrameworkCore;
public class GatewayRegistryService : IGatewayRegistryService
{
private readonly AppDbContext _db;
public GatewayRegistryService(AppDbContext db) => _db = db;
public async Task<WardGateway> RegisterAsync(Guid siteId, RegisterGatewayRequest req)
{
var siteExists = await _db.ClinicalSites.AnyAsync(s => s.Id == siteId);
if (!siteExists)
throw new NotFoundException("Clinical site not found.", "SITE_NOT_FOUND");
var duplicate = await _db.WardGateways.AnyAsync(g =>
g.SiteId == siteId && g.GatewayCode == req.GatewayCode);
if (duplicate)
throw new ConflictException(
$"Gateway code '{req.GatewayCode}' already exists for this site.",
"GATEWAY_CODE_CONFLICT");
var gateway = new WardGateway(siteId, req.GatewayCode, req.Department);
_db.WardGateways.Add(gateway);
await _db.SaveChangesAsync();
return gateway;
}
public async Task<WardGateway> GetByIdAsync(Guid id)
{
var gateway = await _db.WardGateways
.AsNoTracking()
.Include(g => g.Site)
.FirstOrDefaultAsync(g => g.Id == id);
if (gateway is null)
throw new NotFoundException("Ward gateway not found.", "GATEWAY_NOT_FOUND");
return gateway;
}
public async Task<List<WardGateway>> ListBySiteAsync(Guid siteId, string? department)
{
var siteExists = await _db.ClinicalSites.AnyAsync(s => s.Id == siteId);
if (!siteExists)
throw new NotFoundException("Clinical site not found.", "SITE_NOT_FOUND");
var query = _db.WardGateways.AsNoTracking().Where(g => g.SiteId == siteId);
if (!string.IsNullOrWhiteSpace(department))
query = query.Where(g => g.Department == department);
return await query.OrderBy(g => g.GatewayCode).ToListAsync();
}
public async Task<WardGateway> RecordHeartbeatAsync(
Guid gatewayId, GatewayHeartbeatRequest req, string? authenticatedGatewayId)
{
if (authenticatedGatewayId is not null
&& Guid.TryParse(authenticatedGatewayId, out var authId)
&& authId != gatewayId)
throw new ValidationException(
"Gateway id in URL does not match X-Gateway-Id header.",
"GATEWAY_ID_MISMATCH");
var gateway = await _db.WardGateways.FindAsync(gatewayId);
if (gateway is null)
throw new NotFoundException("Ward gateway not found.", "GATEWAY_NOT_FOUND");
var status = GatewayStatusExtensions.FromDbString(req.Status.ToUpperInvariant());
gateway.RecordHeartbeat(status, req.BufferDepth, req.ReportedAtUtc);
await _db.SaveChangesAsync();
return gateway;
}
public async Task MarkOfflineAsync(Guid gatewayId)
{
var gateway = await _db.WardGateways.FindAsync(gatewayId);
if (gateway is null)
throw new NotFoundException("Ward gateway not found.", "GATEWAY_NOT_FOUND");
gateway.MarkOffline();
await _db.SaveChangesAsync();
}
}
@@ -0,0 +1,8 @@
public interface IGatewayRegistryService
{
Task<WardGateway> RegisterAsync(Guid siteId, RegisterGatewayRequest req);
Task<WardGateway> GetByIdAsync(Guid id);
Task<List<WardGateway>> ListBySiteAsync(Guid siteId, string? department);
Task<WardGateway> RecordHeartbeatAsync(Guid gatewayId, GatewayHeartbeatRequest req, string? authenticatedGatewayId);
Task MarkOfflineAsync(Guid gatewayId);
}
@@ -0,0 +1,6 @@
public interface ISiteService
{
Task<ClinicalSite> CreateAsync(CreateSiteRequest req);
Task<ClinicalSite> GetByIdAsync(Guid id);
Task<List<ClinicalSite>> ListAsync();
}
@@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore;
public class SiteService : ISiteService
{
private readonly AppDbContext _db;
public SiteService(AppDbContext db) => _db = db;
public async Task<ClinicalSite> CreateAsync(CreateSiteRequest req)
{
var exists = await _db.ClinicalSites.AnyAsync(s => s.SiteCode == req.SiteCode);
if (exists)
throw new ConflictException(
$"Site code '{req.SiteCode}' is already registered.",
"SITE_CODE_CONFLICT");
var site = new ClinicalSite(req.SiteCode, req.Name, req.Address);
_db.ClinicalSites.Add(site);
await _db.SaveChangesAsync();
return site;
}
public async Task<ClinicalSite> GetByIdAsync(Guid id)
{
var site = await _db.ClinicalSites
.AsNoTracking()
.FirstOrDefaultAsync(s => s.Id == id);
if (site is null)
throw new NotFoundException("Clinical site not found.", "SITE_NOT_FOUND");
return site;
}
public async Task<List<ClinicalSite>> ListAsync() =>
await _db.ClinicalSites.AsNoTracking()
.OrderBy(s => s.SiteCode)
.ToListAsync();
}