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

如何在DDD中处理复杂聚合根不变量?图书借阅系统场景

自定义时长借阅图书管理系统的可用性校验扩展方案

核心需求

  • 支持自定义借阅时长,严格禁止用户预订指定时间段内任意30分钟时隙已被完全占满的图书
  • 可配置订单前后的缓冲期(比如设1天缓冲期,用户要订2022-01-17开始的订单,不能接在2022-01-16结束的订单后面)
  • 库存实时补充后,要立刻允许符合条件的预订
  • 用30min slots拆分时间,通过遍历时隙计算库存可用性

场景示例

现有库存:

Fellowship of the ring (x10)
The Shining (x1)

用户A预订两本书各1本,时间2022-01-01至2022-01-20;用户B尝试在2022-01-15至2022-01-30预订同款书各1本时,必须报错——因为《The Shining》在两个订单重叠的时隙(2022-01-15至2022-01-20)里库存已经耗尽。

当前设计的瓶颈

现有BookOrder聚合根草图:

BookOrder
Members:
DateTime From
DateTime To
Book[] BooksOrdered

Methods:
CanOrder(from, to, books, stocksForTimeSpanWithMargin)
Order(from, to, books, stocksForTimeSpanWithMargin)

扩展性问题

  1. 全量订单传入聚合根:面对1万+图书、日均1万+订单、覆盖未来2年的场景,数据量爆炸,完全扛不住
  2. 仅查指定图书的时隙订单:虽然数据量小了,但校验逻辑被迫移到数据库查询层,聚合根没法确认拿到的是完整数据,只能“盲目”基于传入的数据判断,存在校验漏判的风险

可行解决方案

1. 拆分聚合根,搞图书时隙库存专属聚合

把原来的BookOrder拆成两个核心聚合:

  • BookSlotInventory:以「单本图书+单个30分钟时隙」为聚合根,专门维护这个图书在此时隙里的已预订数量、可用库存数
  • BookOrder:只负责订单的创建、关联图书和时隙,校验逻辑完全依赖BookSlotInventory的结果

执行逻辑:

  • 用户发起预订时,先把目标图书涉及的所有时隙对应的BookSlotInventory实例查出来
  • 逐个时隙校验:已预订数 + 本次预订数 ≤ 当前总库存(含刚补的新书)
  • 同时检查缓冲期:确保预订的起始时间和已有订单的结束时间间隔≥配置值
  • 所有时隙都过审后,批量更新BookSlotInventory的已预订数,再创建BookOrder

2. 数据库层预校验+分布式锁防超卖

如果暂时不想动聚合根结构,就用这个快速方案:

  • 把校验逻辑封装到数据库查询里:写专门的SQL直接查目标图书在指定时隙范围内的最大已预订量,结合当前库存判断能不能订
    -- 示例:查某图书在指定时隙区间内的最大已预订数
    SELECT MAX(bo.quantity) 
    FROM book_order bo
    JOIN book_order_slot bos ON bo.id = bos.order_id
    WHERE bo.book_id = ? 
      AND bos.slot_time BETWEEN ? AND ?
    
  • 加分布式锁:针对要预订的「图书ID+时隙范围」加锁,避免并发下单导致的超卖
  • 聚合根做二次确认:把数据库查出来的校验结果传给聚合根,聚合根只需要再检查缓冲期这类业务规则,不用处理全量订单

3. 缓存层优化查询性能

  • 对高频查询的图书时隙库存做缓存,缓存键用bookId_slotTime,值存已预订数
  • 预订成功、库存补充时立刻更新对应缓存
  • 缓存失效时从数据库拉最新数据,保证数据一致

总结

优先推荐拆分聚合根的方案,符合DDD的设计思路,把时隙库存的校验逻辑闭环在专属聚合里,既保证业务逻辑严谨,又能支撑大规模场景的性能。如果赶进度没法重构,就先上数据库预校验+分布式锁的方案,后续再慢慢调整。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 02:20:33