Skip to content
bitzorcas
中EN

Recipe

实战:为现有业务模块新增一个完整 Feature

掌握在 BitzOrcas.Modern 既有业务模块中新增功能的标准化 5 步流水线:扩展领域聚合根不变量、编写单文件命令切片、声明式配置端点与 RBAC 权限、使用 ICommandRepository 持久化与编写 Testcontainers 集成测试。

Last updated

在实际企业级业务迭代中,为既有模块持续交付新功能(Feature)是研发团队最频繁的日常任务。在传统分层架构中,不同水平的工程师往往会导致代码风格割裂:有人在 Controller 中直拼 SQL,有人在 Service 中手工启动数据库事务,还有人通过全局共享的 God Service 随意读写聚合根私有字段。

BitzOrcas.Modern 确立了工业级标准的 5 步 Feature 交付流水线:

  1. 领域不变量优先(Invariants First):状态变迁必须由聚合根的显式领域方法驱动,严禁外界裸写属性;
  2. 单文件垂直切片(One Use Case, One File):契约、Minimal API 元数据、参数校验规则与业务编排 Handler 高内聚闭环;
  3. 窄端口命令仓储(ICommandRepository):写路径仅依赖最简仓储端口,杜绝在写逻辑中混入复杂 ORM 读查询;
  4. 管道全自动托管:认证、RBAC 细粒度动作权限、三阶校验、事务与事件 Outbox 均由 10 级管道统一处理;
  5. 真实容器集成测试验收:基于 Testcontainers 启动真实数据库实弹验证,彻底拒绝 Mock 鱼目混珠。

本指南以法律科技核心场景——在案件管理模块(BitzOrcas.Modules.Legal)中新增**“案件审理阶段流转(AdvanceMatterStageCommand)”**为例,演示完整的交付全景。

新增 Feature 标准 5 步交付流水线

1. 扩展领域聚合
(MatterIntake 业务状态方法)

2. 编写单文件切片
(AdvanceMatterStageCommand)

3. 声明路由与鉴权
([GenerateEndpoint])

4. 编写纯函数规则
(IRequestRule 校验)

5. 容器化集成测试
(Testcontainers 验收)


第一步:在领域聚合根中增加业务状态流转方法

聚合根是一致性边界与状态不变量的唯一守护者。严禁在 Handler 中直接对聚合根字段进行赋值,所有状态变更必须封装为聚合根的领域行为:

src/Modules/Legal/BitzOrcas.Modules.Legal.Domain/MatterIntake.cs (片段)
using System;
using BitzOrcas.Domain.Entities;
using BitzOrcas.Domain.Results;
using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Modules.Legal.Domain;
/// <summary>
/// 案件基础状态枚举
/// </summary>
public enum MatterStatus
{
Draft = 1,
Active = 2,
Closed = 3,
Archived = 4
}
/// <summary>
/// 案件审理阶段定义枚举
/// </summary>
public enum MatterStage
{
/// <summary>
/// 立案受理与管辖权确认
/// </summary>
IntakeAccepted = 1,
/// <summary>
/// 证据交换与质证准备
/// </summary>
EvidenceDiscovery = 2,
/// <summary>
/// 法庭开庭审理
/// </summary>
TrialHearing = 3,
/// <summary>
/// 判决生效与强制执行
/// </summary>
Enforcement = 4
}
/// <summary>
/// 案件审理阶段推进领域事件
/// </summary>
public sealed record MatterStageAdvancedDomainEvent(
string MatterId,
string MatterCode,
string TenantId,
int PreviousStage,
int NewStage,
string OperatorId,
DateTime OccurredAtUtc) : IDomainEvent;
/// <summary>
/// 案件阶段流转强类型错误字典
/// </summary>
public static class MatterStageErrors
{
public static readonly Error InvalidStatusForAdvance = Error.Conflict(
"Matter.InvalidStatusForAdvance", "当前案件未处于正式立案审理状态,禁止推进审理阶段。");
public static readonly Error StageRegressionForbidden = Error.Validation(
"Matter.StageRegressionForbidden", "目标审理阶段必须高于当前所处阶段,不允许向后倒退。");
public static readonly Error StageRemarksRequired = Error.Validation(
"Matter.StageRemarksRequired", "流转备注说明不能为空且长度不能超过 500 个字符。");
public static readonly Error IdRequired = Error.Validation(
"Matter.IdRequired", "案件标识不能为空。");
public static readonly Error InvalidTargetStage = Error.Validation(
"Matter.InvalidTargetStage", "目标审理阶段取值超出合法枚举范围。");
public static readonly Error RemarksInvalid = Error.Validation(
"Matter.RemarksInvalid", "阶段流转备注不能为空且长度不能超过 500 字。");
}
public sealed partial class MatterIntake : TenantAggregateRoot<string>
{
/// <summary>
/// 案件当前业务状态
/// </summary>
[BitzColumn(Description = "案件业务状态")]
public MatterStatus Status { get; private set; } = MatterStatus.Active;
/// <summary>
/// 当前所处审理阶段
/// </summary>
[BitzColumn(Description = "当前审理阶段")]
public MatterStage CurrentStage { get; private set; } = MatterStage.IntakeAccepted;
/// <summary>
/// 推进案件审理阶段领域行为
/// </summary>
/// <param name="targetStage">目标阶段枚举。</param>
/// <param name="stageRemarks">流转备注说明。</param>
/// <param name="operatorId">经办人员标识。</param>
/// <returns>操作结果。</returns>
public Result AdvanceStage(MatterStage targetStage, string stageRemarks, string operatorId)
{
// 1. 校验案件基础状态不变量:只有已立案 (Status == MatterStatus.Active) 的案件允许流转
if (Status != MatterStatus.Active)
{
return Result.Failure(MatterStageErrors.InvalidStatusForAdvance);
}
// 2. 校验阶段不可逆流转 (不允许向后倒退阶段)
if ((int)targetStage <= (int)CurrentStage)
{
return Result.Failure(MatterStageErrors.StageRegressionForbidden);
}
// 3. 校验阶段备注长度业务约束
if (string.IsNullOrWhiteSpace(stageRemarks) || stageRemarks.Length > 500)
{
return Result.Failure(MatterStageErrors.StageRemarksRequired);
}
var previousStage = (int)CurrentStage;
// 4. 执行状态变更
CurrentStage = targetStage;
// 5. 登记领域事件 (在事务提交后由框架自动派发)
Raise(new MatterStageAdvancedDomainEvent(
MatterId: Id,
MatterCode: MatterCode,
TenantId: TenantId,
PreviousStage: previousStage,
NewStage: (int)targetStage,
OperatorId: operatorId,
OccurredAtUtc: DateTime.UtcNow));
return Result.Success();
}
}

第二步:编写自包含单文件垂直切片(One Use Case, One File)

在 src/Modules/Legal/BitzOrcas.Modules.Legal/Application/Matters/ 目录下创建 AdvanceMatterStageCommand.cs。 该文件包含完整的命令契约、Minimal API 路由绑定、纯函数校验规则与业务处理器:

src/Modules/Legal/BitzOrcas.Modules.Legal/Application/Matters/AdvanceMatterStageCommand.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using BitzOrcas.Application.Abstractions;
using BitzOrcas.Application.Attributes;
using BitzOrcas.Application.Security;
using BitzOrcas.Application.Validation;
using BitzOrcas.Domain.Abstractions;
using BitzOrcas.Domain.Results;
using BitzOrcas.Domain.Tenancy;
using BitzOrcas.Modules.Legal.Domain;
namespace BitzOrcas.Modules.Legal.Application.Matters;
/// <summary>
/// 推进案件审理阶段命令
/// </summary>
/// <remarks>
/// 编译期由 Roslyn 增量生成器自动绑定为 Minimal API 端点,并挂载细粒度动作权限。
/// </remarks>
[GenerateEndpoint(
Route = "api/legal/matters/{matterId}/advance-stage",
Method = EndpointMethod.Post,
RequirePermission = "legal.matters.advance-stage", // 细粒度 RBAC 权限
Summary = "推进案件审理阶段",
Description = "将民商事案件推进至证据交换、开庭辩论或强制执行阶段并记录审计流水")]
public sealed record AdvanceMatterStageCommand(
string MatterId,
MatterStage TargetStage,
string StageRemarks) : ICommand<Result>;
/// <summary>
/// 案件阶段流转纯函数一阶校验规则
/// </summary>
/// <remarks>
/// 在进入 Handler 之前由 10 级管道第 5 级(ValidationBehavior)自动执行,阻断不合法入参。
/// </remarks>
public sealed class AdvanceMatterStageCommandRule : IRequestRule<AdvanceMatterStageCommand>
{
/// <summary>
/// 异步校验参数合法性
/// </summary>
public ValueTask<Result> ValidateAsync(AdvanceMatterStageCommand command, CancellationToken cancellationToken)
{
// 1. 案件主键非空校验
if (string.IsNullOrWhiteSpace(command.MatterId))
{
return ValueTask.FromResult(Result.Failure(MatterStageErrors.IdRequired));
}
// 2. 目标阶段枚举有效性校验
if (!Enum.IsDefined(typeof(MatterStage), command.TargetStage))
{
return ValueTask.FromResult(Result.Failure(MatterStageErrors.InvalidTargetStage));
}
// 3. 阶段流转备注非空与长度限制
if (string.IsNullOrWhiteSpace(command.StageRemarks) || command.StageRemarks.Length > 500)
{
return ValueTask.FromResult(Result.Failure(MatterStageErrors.RemarksInvalid));
}
return ValueTask.FromResult(Result.Success());
}
}
/// <summary>
/// 案件阶段流转命令执行处理器
/// </summary>
/// <remarks>
/// 纯粹的领域编排,事务、事件分发与变更审计均由框架 10 级管道自动治理。
/// 依赖规范:写侧仅依赖 <see cref="ICommandRepository{TAggregateRoot, TId}"/> 窄端口。
/// </remarks>
public sealed class AdvanceMatterStageCommandHandler : ICommandHandler<AdvanceMatterStageCommand, Result>
{
private readonly ICommandRepository<MatterIntake, string> _repository;
private readonly ICurrentUser _currentUser;
/// <summary>
/// 初始化命令处理器
/// </summary>
public AdvanceMatterStageCommandHandler(
ICommandRepository<MatterIntake, string> repository,
ICurrentUser currentUser)
{
_repository = repository;
_currentUser = currentUser;
}
/// <summary>
/// 执行业务编排
/// </summary>
public async Task<Result> Handle(AdvanceMatterStageCommand request, CancellationToken cancellationToken)
{
// 1. 从命令仓储恢复聚合根 (管道与仓储自动叠加租户防穿透条件)
var matterResult = await _repository.FindAsync(request.MatterId, cancellationToken);
if (matterResult.IsFailure)
{
return Result.Failure(matterResult.Error);
}
var matter = matterResult.GetValueOrThrow();
// 2. 调用聚合根领域行为推进阶段
var advanceResult = matter.AdvanceStage(
targetStage: request.TargetStage,
stageRemarks: request.StageRemarks,
operatorId: _currentUser.Id);
if (advanceResult.IsFailure)
{
return advanceResult;
}
// 3. 提交持久化更新 (由管道外层 UnitOfWorkBehavior 自动完成事务提交与审计快照)
return await _repository.SaveAsync(matter, cancellationToken);
}
}

第三步:编写 Testcontainers 容器化实弹集成测试

在 tests/BitzOrcas.Integration.Tests/Legal/ 目录下编写真实容器集成测试。 测试依托官方 SHA-256 锁定的 SQL Server 2022 镜像与 BitzOrcasWebApplicationFactory,杜绝任何本地环境差异:

tests/BitzOrcas.Integration.Tests/Legal/AdvanceMatterStageTests.cs
using System.Net;
using System.Net.Http.Json;
using System.Threading.Tasks;
using BitzOrcas.Domain.Results;
using BitzOrcas.Integration.Tests.Fixtures;
using BitzOrcas.Modules.Legal.Application.Matters;
using BitzOrcas.Modules.Legal.Domain;
using FluentAssertions;
using Xunit;
namespace BitzOrcas.Integration.Tests.Legal;
/// <summary>
/// 案件审理阶段流转全链路容器化集成测试
/// </summary>
public sealed class AdvanceMatterStageTests : IClassFixture<BitzOrcasWebApplicationFactory>
{
private readonly BitzOrcasWebApplicationFactory _factory;
public AdvanceMatterStageTests(BitzOrcasWebApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task AdvanceStage_WithValidTargetStage_ShouldSucceedAndPersist()
{
// 1. 创建具备测试租户凭证的 HttpClient
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-alpha-lawfirm");
// 2. 准备阶段流转命令
var command = new AdvanceMatterStageCommand(
MatterId: "MAT-2026-0001",
TargetStage: MatterStage.TrialHearing,
StageRemarks: "已完成第一次庭前证据交换与质证,法庭决定正式进入一审开庭审理。");
// 3. 发起 HTTP POST 请求
var response = await client.PostAsJsonAsync(
"api/legal/matters/MAT-2026-0001/advance-stage",
command);
// 4. 断言 HTTP 状态码为 200 OK
response.StatusCode.Should().Be(HttpStatusCode.OK);
// 5. 校验统一业务结果对象
var result = await response.Content.ReadFromJsonAsync<Result>();
result.Should().NotBeNull();
result!.IsSuccess.Should().BeTrue();
}
[Fact]
public async Task AdvanceStage_WithStageRegression_ShouldReturnValidationError()
{
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-alpha-lawfirm");
// 试图向后倒退审理阶段 (目标阶段等于或低于当前阶段)
var regressionCommand = new AdvanceMatterStageCommand(
MatterId: "MAT-2026-0001",
TargetStage: MatterStage.IntakeAccepted,
StageRemarks: "试图倒退回立案阶段。");
var response = await client.PostAsJsonAsync(
"api/legal/matters/MAT-2026-0001/advance-stage",
regressionCommand);
// 断言业务冲突校验拦截 (400 校验错误)
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var result = await response.Content.ReadFromJsonAsync<Result>();
result.Should().NotBeNull();
result!.IsFailure.Should().BeTrue();
result.Error.Code.Should().Be("Matter.StageRegressionForbidden");
}
}

第四步:静态编译与架构守卫断言

在代码落地后,主动在终端执行编译与测试命令,确保零编译错误与警告:

Terminal window
# 1. 编译全解决方案 (启用严格空安全与 Roslyn 生成器)
dotnet build BitzOrcas.Modern.slnx -c Release
# 2. 运行模块边界架构守卫测试
dotnet test tests/BitzOrcas.Architecture.Tests/ --filter FullyQualifiedName~ModuleBoundaryTests
# 3. 运行实弹集成测试 (拉起 Testcontainers)
dotnet test tests/BitzOrcas.Integration.Tests/ --filter FullyQualifiedName~AdvanceMatterStageTests

总结

通过标准 5 步交付流水线,为现有模块新增 Feature 具备以下确定性:

  • 开发零冲突:业务用例自包含于单一文件,无需修改任何共享的 God Service;
  • 管线全自动:RBAC 鉴权、参数校验、租户隔离与事务管理由 10 级管道统一管控;
  • 写侧窄端口:严格使用 ICommandRepository<T, TId>,杜绝 ORM 泄漏与写放大;
  • 可信交付:基于真实容器集成测试,杜绝运行时故障。

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%