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

聚合根外执行业务规则:DDD餐厅领域菜单模块规则实现咨询

问题解决方案

规则1:同餐厅同一时间仅一份Menu处于激活状态

你现有代码中的Restaurant聚合根本身就持有ActiveMenuId属性,该规则天然属于Restaurant的职责边界,无需将仓储传入Menu实体,调整逻辑如下:

  • 移除聚合根对仓储的依赖:Menu的Activate方法仅校验自身内部规则(如必须包含至少一个分组、分组数据一致性),Restaurant的ChangeActiveMenu方法仅校验聚合内部规则(如待激活菜单属于当前餐厅、未处于激活状态)
  • 跨聚合逻辑放到应用层编排:在调用ChangeActiveMenu前,先通过Menu仓储取出待激活的Menu实例,调用Activate完成自身校验,再将MenuId传入Restaurant的ChangeActiveMenu方法完成激活状态替换
  • 并发兜底:在数据库层面给菜单表添加(restaurant_id, is_active)联合唯一索引,未激活菜单的is_active字段设为null(null不参与唯一索引校验),从存储层面保证不会出现多份同时激活的菜单

规则2:Menu内部名称同餐厅唯一

该类跨实体的集合校验有三种成熟的落地方式,可根据业务场景选择:

  1. 数据库唯一索引兜底:直接给菜单表添加(restaurant_id, internal_name)联合唯一索引,出现重复插入/更新时捕获数据库异常转换为业务错误返回,实现成本最低,适合并发度不高的内部管理场景
  2. 应用层前置校验:在创建菜单、修改菜单名称的业务逻辑执行前,先调用Menu仓储的ExistsByRestaurantIdAndInternalName方法做唯一性校验,通过后再执行后续操作,配合数据库索引兜底可覆盖绝大多数场景
  3. 领域服务封装复用:如果多段业务逻辑都需要用到名称唯一性校验,可以将校验逻辑封装到MenuUniquenessCheckDomainService中,由应用层调用该服务完成校验,不要将领域服务传入Menu实体内部,避免实体和外部服务耦合

调整后的代码示例

领域层代码

public class Menu : AggregateRoot<MenuId>
{
    public string InternalName { get; private set; }
    public RestaurantId RestaurantId { get; private set; }
    public IReadOnlyList<Group> Groups => _groups;
    private List<Group> _groups = new();

    internal void Activate()
    {
        CheckRule(new ActiveMenuMustHaveAtLeastOneGroup(_groups));
        _groups.ForEach(x => x.CheckConsistency());
    }

    public void ChangeInternalName(string newInternalName)
    {
        // 仅做自身属性基础校验,不依赖外部资源
        CheckRule(new InternalNameCannotBeEmptyRule(newInternalName));
        CheckRule(new InternalNameLengthValidRule(newInternalName));
        InternalName = newInternalName;
    }
}

public class Restaurant : AggregateRoot<RestaurantId>
{
    public MenuId ActiveMenuId { get; private set; }
    public IReadOnlyList<MenuId> MenuIds => _menuIds;
    private List<MenuId> _menuIds = new();

    public void ChangeActiveMenu(MenuId newActiveMenuId)
    {
        CheckRule(new MenuBelongsToRestaurantRule(_menuIds, newActiveMenuId));
        CheckRule(new CannotActivateActiveMenuRule(ActiveMenuId, newActiveMenuId));
        ActiveMenuId = newActiveMenuId;
    }
}

应用层代码示例

public class RestaurantMenuAppService
{
    private readonly IRestaurantRepository _restaurantRepository;
    private readonly IMenuRepository _menuRepository;
    private readonly IUnitOfWork _unitOfWork;

    // 构造注入省略

    public async Task ChangeActiveMenuAsync(RestaurantId restaurantId, MenuId menuId, CancellationToken ct)
    {
        var restaurant = await _restaurantRepository.GetByIdAsync(restaurantId, ct);
        if (restaurant == null) throw new BusinessException("餐厅不存在");

        var menu = await _menuRepository.GetByIdAsync(menuId, ct);
        if (menu == null) throw new BusinessException("菜单不存在");

        // 执行菜单自身激活校验
        menu.Activate();
        // 执行餐厅激活菜单变更
        restaurant.ChangeActiveMenu(menuId);

        await _unitOfWork.SaveChangesAsync(ct);
    }

    public async Task ChangeMenuInternalNameAsync(MenuId menuId, string newInternalName, CancellationToken ct)
    {
        var menu = await _menuRepository.GetByIdAsync(menuId, ct);
        if (menu == null) throw new BusinessException("菜单不存在");

        // 前置校验名称唯一性
        var nameExists = await _menuRepository.ExistsByRestaurantIdAndNameAsync(
            menu.RestaurantId, 
            newInternalName, 
            excludeMenuId: menuId, 
            cancellationToken: ct);
        if (nameExists) throw new BusinessException("同餐厅下已存在相同名称的菜单");

        menu.ChangeInternalName(newInternalName);
        await _unitOfWork.SaveChangesAsync(ct);
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 15:15:05