You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于DDD实现.NET 8 WebAPI座位预订超时自动持久化

DDD下座位超时自动释放的正确实现方案

在DDD和垂直切片架构的约束下,核心原则是领域模型只聚焦业务规则,持久化与外部触发逻辑完全剥离。针对你提出的座位超时自动从Pending转为Vacant的需求,以下是几种符合DDD理念的实现方案,按贴合领域驱动的优先级排序:

方案一:领域事件 + 延迟任务调度

这是最符合DDD思想的方案——通过领域事件传递业务意图,由外部组件处理超时逻辑,完全不污染领域模型。

步骤1:在领域模型中发布事件

当座位进入Pending状态时,领域对象发布一个领域事件,表达“该座位需要在指定时间后检查并释放”的业务意图。修改Seat的Reserve方法:

public Result Reserve(ushort pendingTimeoutInSeconds)
{
    if (Occupancy != OccupancyState.Vacant) return SeatError.InvalidAction;

    Occupancy = OccupancyState.Pending;
    OccupancyStateExpiration = DateTime.UtcNow.AddSeconds(pendingTimeoutInSeconds);

    // 发布领域事件(用内存事件集合,避免领域模型依赖外部服务)
    DomainEvents.Add(new SeatPendingExpiredEvent(Id, OccupancyStateExpiration.Value));

    return Result.Success();
}

// 领域事件定义(放在领域层)
public record SeatPendingExpiredEvent(Guid SeatId, DateTime ExpirationTime);

// 内存事件发布器(领域层工具类)
public static class DomainEvents
{
    public static List<object> Events { get; } = new();
    public static void Add(object eventObj) => Events.Add(eventObj);
    public static void Clear() => Events.Clear();
}

步骤2:应用层订阅并调度延迟任务

在保存领域对象到数据库后,应用层取出领域事件,通过调度组件(如Hangfire、Quartz,或.NET 8自带的BackgroundService结合延迟队列)创建延迟任务,到超时时间触发处理逻辑:

// 应用层服务:处理座位预订命令
public class ReserveSeatCommandHandler
{
    private readonly ISeatRepository _seatRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IBackgroundJobClient _backgroundJobClient;

    public ReserveSeatCommandHandler(ISeatRepository seatRepository, IUnitOfWork unitOfWork, IBackgroundJobClient backgroundJobClient)
    {
        _seatRepository = seatRepository;
        _unitOfWork = unitOfWork;
        _backgroundJobClient = backgroundJobClient;
    }

    public async Task<Result> Handle(Guid seatId, ushort pendingTimeoutInSeconds)
    {
        var seat = await _seatRepository.GetByIdAsync(seatId);
        if (seat == null) return SeatError.NotFound;

        var result = seat.Reserve(pendingTimeoutInSeconds);
        if (!result.IsSuccess) return result;

        await _unitOfWork.SaveChangesAsync();

        // 处理领域事件,调度延迟任务
        foreach (var domainEvent in DomainEvents.Events)
        {
            if (domainEvent is SeatPendingExpiredEvent expiredEvent)
            {
                _backgroundJobClient.Schedule<SeatExpirationProcessor>(
                    p => p.ProcessExpiredSeat(expiredEvent.SeatId),
                    expiredEvent.ExpirationTime - DateTime.UtcNow);
            }
        }
        DomainEvents.Clear();

        return result;
    }
}

// 超时处理处理器
public class SeatExpirationProcessor
{
    private readonly ISeatRepository _seatRepository;
    private readonly IUnitOfWork _unitOfWork;

    public SeatExpirationProcessor(ISeatRepository seatRepository, IUnitOfWork unitOfWork)
    {
        _seatRepository = seatRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task ProcessExpiredSeat(Guid seatId)
    {
        var seat = await _seatRepository.GetByIdAsync(seatId);
        if (seat == null) return;

        // 双重检查:避免并发操作导致的状态不一致
        if (seat.Occupancy == Seat.OccupancyState.Pending && 
            seat.OccupancyStateExpiration <= DateTime.UtcNow)
        {
            seat.Release();
            await _unitOfWork.SaveChangesAsync();
        }
    }
}

方案二:定时批量扫描后台服务

如果不想引入延迟任务调度组件,可采用定期扫描数据库的方案,适合小型系统或对延迟容忍度较高的场景。

实现代码

创建一个后台服务,定期查询数据库中已过期的Pending座位,调用领域模型的Release()方法更新状态:

public class ExpiredSeatCleanupService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    // 扫描间隔可配置,比如1分钟
    private readonly TimeSpan _scanInterval = TimeSpan.FromMinutes(1);

    public ExpiredSeatCleanupService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = _scopeFactory.CreateScope();
            var seatRepository = scope.ServiceProvider.GetRequiredService<ISeatRepository>();
            var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();

            // 查询所有已过期的Pending座位
            var expiredSeats = await seatRepository.GetExpiredPendingSeatsAsync(DateTime.UtcNow);
            foreach (var seat in expiredSeats)
            {
                seat.Release();
            }

            await unitOfWork.SaveChangesAsync();

            await Task.Delay(_scanInterval, stoppingToken);
        }
    }
}

// 仓储层扩展方法
public interface ISeatRepository
{
    Task<List<Seat>> GetExpiredPendingSeatsAsync(DateTime currentTime);
    // 其他仓储方法...
}

// EF Core仓储实现
public class EfSeatRepository : ISeatRepository
{
    private readonly CinemaDbContext _dbContext;

    public EfSeatRepository(CinemaDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<List<Seat>> GetExpiredPendingSeatsAsync(DateTime currentTime)
    {
        return await _dbContext.Seats
            .Where(s => s.Occupancy == Seat.OccupancyState.Pending && 
                        s.OccupancyStateExpiration <= currentTime)
            .ToListAsync();
    }
}

方案三:业务操作时的即时补偿

作为前两种方案的补充,在任何涉及座位的业务操作(如预订、确认、查询)之前,先检查并处理过期状态,避免用户看到不一致的状态:

public class SeatService
{
    private readonly ISeatRepository _seatRepository;
    private readonly IUnitOfWork _unitOfWork;

    public SeatService(ISeatRepository seatRepository, IUnitOfWork unitOfWork)
    {
        _seatRepository = seatRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<Result> ReserveSeat(Guid seatId, ushort pendingTimeoutInSeconds)
    {
        var seat = await _seatRepository.GetByIdAsync(seatId);
        if (seat == null) return SeatError.NotFound;

        // 先处理过期状态
        await ProcessExpiredSeatIfNeeded(seat);

        return seat.Reserve(pendingTimeoutInSeconds);
    }

    public async Task<Seat> GetSeatById(Guid seatId)
    {
        var seat = await _seatRepository.GetByIdAsync(seatId);
        if (seat == null) return null;

        await ProcessExpiredSeatIfNeeded(seat);
        return seat;
    }

    private async Task ProcessExpiredSeatIfNeeded(Seat seat)
    {
        if (seat.Occupancy == Seat.OccupancyState.Pending && 
            seat.OccupancyStateExpiration <= DateTime.UtcNow)
        {
            seat.Release();
            await _unitOfWork.SaveChangesAsync();
        }
    }
}

核心原则总结

  1. 领域模型只负责业务规则:所有状态修改必须通过Seat的Release()方法,而非直接更新数据库字段,确保业务逻辑的一致性。
  2. 外部逻辑与领域分离:超时触发、持久化操作均放在应用层或基础设施层,领域模型不依赖任何外部框架或服务。
  3. 状态一致性保障:无论采用哪种方案,都必须做双重检查(确认座位当前状态为Pending且已过期),避免并发操作导致的错误。

内容的提问来源于stack exchange,提问作者user23421787

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 20:00:21