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

在DDD+事件溯源架构中如何处理时效性业务规则?

OTP时效性逻辑实现方案选择(事件溯源+DDD场景)

我们有一个代表一次性密码(OTP)的实体,15分钟后过期。系统采用事件溯源架构并遵循DDD原则,定义的核心事件如下:

public record OtpCreatedEvent(
    string MobilePhoneNumber,
    string Password,
    IPAddress RequesterIp,
    DateTimeOffset OccurredAt
) : IEvent;

业务规则要求:

  • 创建新OTP时(CreateOtp命令):
    1. 单个客户端(按IP标识)1小时内为任意手机号创建的OTP不得超过15个;
    2. 同一手机号的两次OTP请求间隔至少20秒。
  • 校验OTP时(VerifyOtp命令):需获取该手机号所有未过期的OTP,校验输入密码是否匹配其中任意一个。

针对时效性逻辑,目前有两种可行方案:

  1. 命令调用时,将时效逻辑整合到数据查询中,示例:
    var nonExpiredOtps = dataStore.Otps.Where(otp => otp.CreatedAt > DateTime.Now.AddMinutes(-15))
    
  2. 创建新OTP时,调度一个15分钟后执行的ExpireOtp命令,触发OtpExpiredEvent事件标记OTP过期。

方案2需要额外基础设施支持,方案1存在代码不够优雅的问题,想请教哪种方案更推荐及原因,同时是否存在其他方案?


推荐方案及原因

优先选择方案1,原因如下:

  • 贴合事件溯源核心设计:OTP过期是时间驱动的属性过滤,而非业务行为触发的状态变更。事件溯源的事件应记录业务动作带来的状态转换,过期不属于这类行为,没必要用事件标记。
  • 降低系统复杂度:方案1无需依赖定时调度基础设施(如Quartz、Hangfire),避免了调度失败(服务宕机、任务丢失)导致的状态不一致问题,减少运维成本。
  • 可优化代码优雅性:时效逻辑可封装到专门的查询服务(如IOtpQueryService)中,把过滤、频率统计等逻辑统一封装,业务代码只需调用服务方法(如GetActiveOtpsForPhone(string phone)、CheckIpRateLimit(IPAddress ip)),避免业务逻辑中散落查询条件。

方案2仅适合需要主动触发过期后业务动作的场景(如给用户发送OTP过期提醒、清理关联业务资源),若只是为标记过期引入调度,属于过度设计。

其他可选方案

1. 基于事件投影维护活跃OTP读模型

在事件溯源架构中,通过订阅OtpCreatedEvent构建专门的读模型:

  • 投影层接收事件后,将OTP数据写入读模型并记录创建时间;
  • 定期(如每分钟)清理读模型中已过期的OTP,或查询时自动过滤过期数据;
  • 业务代码直接查询该读模型,无需重复编写时效过滤逻辑,符合CQRS分工,同时简化业务代码。

2. 结合缓存实现高并发时效管理

用Redis等缓存存储未过期的OTP:

  • 创建OTP时存入缓存并设置15分钟过期时间,同时用缓存原子命令实现频率限制:
    • IP的1小时15个限制:用INCR统计请求次数,设置1小时过期时间;
    • 手机号的20秒间隔:用SET存储最后请求时间,设置20秒过期时间,仅当不存在时允许写入;
  • 校验OTP时直接从缓存读取,缓存自动处理过期逻辑,性能更高,适合高并发场景,同时简化业务代码的时效处理。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 11:22:49