在许多企业级 SaaS 系统的早期演进中,开发团队经常陷入**“租户校验分支污染”的泥潭**:
- 代码级硬编码污染:为了满足不同租户的个性化风控要求,开发者在同一个验证器中写满了
if (tenantId == "1000001") { ... }甚至When(t => ...),导致核心验证逻辑迅速劣化为无法测试的混沌状态; - 发版依赖与运维阻断:当大客户提出“针对涉案标的额 $\ge 5000$ 万元的跨国仲裁,必须强制校验并上传《利益冲突豁免函(Conflict Waiver)》”时,研发团队竟不得不全量构建、回归测试并重新发布整个后端 API 镜像;
- 执业道德与监管零容忍:在法律科技与金融级系统中,若代理了与既有受托客户存在对抗性利益冲突的对方当事人,将直接面临监管吊销执业资格与民事巨额索赔。
BitzOrcas.Modern 独创“4 阶动态业务校验黄金范式(4-Tier Validation Pattern)”。它将验证职责严格解耦为四个层级:
- Tier 1 (全局基础通用规则):编译期绑定的强类型基础不变量;
- Tier 2(A) (组合与级联策略):针对特定行业形态的复合型跨实体阻断规则;
- Tier 2(B) (租户动态热配置策略):无需重新发版即可通过配置中心秒级热更新的阈值策略;
- Tier 3 (现场实施无代码引擎):交付与技术支持人员在运营控制台直接配置的高阶 JSON 正则校验。
本教程将以真实的律所民商事诉讼立案与利益冲突阻断审查为用例,带你完整实现一套生产级的多租户四阶动态校验体系。
多租户四阶动态校验执行全景
第一步:定义立案命令契约(Command Contract)
立案命令定义了标的额、委托方、争议相对方、涉外标记及可选的豁免证明标识:
using BitzOrcas.Application.Authorization;using BitzOrcas.Domain.Abstractions;using BitzOrcas.Domain.Results;using BitzOrcas.Endpoint.Attributes;using Mediator;
namespace BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake;
public enum MatterCategory{ Litigation = 1, // 民商事诉讼 CrossBorder = 2, // 涉外争议与跨国仲裁 NonLitigation = 3 // 非诉与企业合规}
[GenerateEndpoint(HttpRoute.Post, "/api/legal/matters", Tag = "LegalMatter")]public sealed record CreateMatterIntakeCommand( string MatterTitle, string ClientName, string OpposingParty, decimal ClaimAmount, MatterCategory Category, string? ConflictWaiverDocumentId = null) : ICommand<Result<string>>, IAuthorizedRequest{ public ResourceDescriptor Resource { get; } = new("Legal", "Matter"); public AuthorizationAction Action { get; } = AuthorizationAction.Create;}第二步:实现 Tier 1 全局基础规则(Universal Base Rules)
所有租户必须强制遵守的通用不变量(如标题非空、标的额必须为正数、委托人不可与相对人同名),通过实现 IRequestRule<TRequest> 承载:
using BitzOrcas.Domain.Results;
namespace BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake.Rules;
/// <summary>/// 案件立案校验稳定错误契约/// </summary>public static class LegalValidationErrors{ /// <summary> /// 案件标的名称非法 /// </summary> public static readonly Error InvalidTitle = Error.Validation("Legal.Validation.InvalidTitle", "案件标的名称不能为空且长度不得超过 200 个字符。");
/// <summary> /// 委托当事人与相对方名称必填 /// </summary> public static readonly Error PartiesRequired = Error.Validation("Legal.Validation.PartiesRequired", "案件委托当事人与争议相对方名称均属于必填项。");
/// <summary> /// 当事人与相对方为同一主体冲突 /// </summary> public static readonly Error AdversaryConflict = Error.Conflict("Legal.Validation.AdversaryConflict", "委托人与相对方主体同名,存在根本性法定利益冲突。");
/// <summary> /// 争议标的金额必须大于零 /// </summary> public static readonly Error InvalidAmount = Error.Validation("Legal.Validation.InvalidAmount", "争议标的金额必须为大于零的数值。");
/// <summary> /// 跨国仲裁业务大额争议必须提供豁免函 /// </summary> public static readonly Error WaiverRequired = Error.Validation("Legal.Validation.WaiverRequired", "涉案标的额大于等于 5,000 万元的跨国仲裁业务,必须级联提交客户签署的《利益冲突豁免函(Conflict Waiver)》。");}using BitzOrcas.Application.Validation;using BitzOrcas.Domain.Results;
namespace BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake.Rules;
/// <summary>/// Tier 1:所有租户强制生效的不可变基础规则/// </summary>public sealed class CreateMatterIntakeBaseRule : IRequestRule<CreateMatterIntakeCommand>{ public ValueTask<Result> ValidateAsync(CreateMatterIntakeCommand request, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(request.MatterTitle) || request.MatterTitle.Length > 200) { return ValueTask.FromResult(Result.Failure(LegalValidationErrors.InvalidTitle)); }
if (string.IsNullOrWhiteSpace(request.ClientName) || string.IsNullOrWhiteSpace(request.OpposingParty)) { return ValueTask.FromResult(Result.Failure(LegalValidationErrors.PartiesRequired)); }
// 基础不变量:原告与被告不可为同一法律主体 if (string.Equals(request.ClientName.Trim(), request.OpposingParty.Trim(), StringComparison.OrdinalIgnoreCase)) { return ValueTask.FromResult(Result.Failure(LegalValidationErrors.AdversaryConflict)); }
if (request.ClaimAmount <= 0) { return ValueTask.FromResult(Result.Failure(LegalValidationErrors.InvalidAmount)); }
return ValueTask.FromResult(Result.Success()); }}第三步:实现 Tier 2 租户特化策略(ITenantValidationStrategy)
在 BitzOrcas.Modern 中,多租户差异化逻辑不侵入基础规则。通过实现 ITenantValidationStrategy<TRequest>,系统会在运行时根据当前租户快照(ICurrentTenant)动态拉取专属于该租户的追加规则集:
using BitzOrcas.Application.Abstractions.Tenancy;using BitzOrcas.Application.Validation;using BitzOrcas.Domain.Results;using BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake.Rules;
namespace BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake.Strategies;
/// <summary>/// Tier 2:租户定制化多层级校验策略分派器/// </summary>public sealed class TenantMatterValidationStrategy : ITenantValidationStrategy<CreateMatterIntakeCommand>{ public IReadOnlyList<IRequestRule<CreateMatterIntakeCommand>> GetTenantRules(ICurrentTenant currentTenant) { var tenant = currentTenant.Tenant; if (!tenant.IsAvailable) return [];
var rules = new List<IRequestRule<CreateMatterIntakeCommand>>();
// 租户 1000001(红圈涉外所):大额案件必须上传冲突豁免书,且标的额超过 5000 万必须通过涉外审查 if (tenant.EffectiveTenantId == "1000001") { rules.Add(new HighValueCrossBorderWaiverRule()); }
return rules; }}
/// <summary>/// Tier 2(A):特定大所的组合级联风控规则/// </summary>internal sealed class HighValueCrossBorderWaiverRule : IRequestRule<CreateMatterIntakeCommand>{ public ValueTask<Result> ValidateAsync(CreateMatterIntakeCommand request, CancellationToken cancellationToken) { // 规则:当案件为涉外争议且标的额超过 5,000 万元时,必须提供经公证的利益冲突豁免书 if (request.Category == MatterCategory.CrossBorder && request.ClaimAmount >= 50_000_000.00m) { if (string.IsNullOrWhiteSpace(request.ConflictWaiverDocumentId)) { return ValueTask.FromResult(Result.Failure(LegalValidationErrors.WaiverRequired)); } }
return ValueTask.FromResult(Result.Success()); }}第四步:端到端多租户差异化校验集成测试
编写集成测试,验证租户特化策略在隔离环境下的真实表现:
using System.Net;using System.Net.Http.Json;using BitzOrcas.Domain.Results;using BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake;using BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake.Rules;using Shouldly;using Xunit;
public sealed class MultiTenantValidationIntegrationTests : IClassFixture<CustomWebApplicationFactory>{ private readonly CustomWebApplicationFactory _factory;
public MultiTenantValidationIntegrationTests(CustomWebApplicationFactory factory) { _factory = factory; }
[Fact] public async Task StandardTenant_SubmittingHighValueMatter_WithoutWaiver_ShouldSucceed() { // 1. 使用普通中小律所租户(租户 1000002)发起 6000 万诉讼 var client = _factory.CreateAuthenticatedClient(tenantId: "1000002", role: "SuperAdmin"); var command = new CreateMatterIntakeCommand( MatterTitle: "常规工程款结算纠纷诉讼", ClientName: "某建筑工程有限公司", OpposingParty: "某地方城市建设发展集团", ClaimAmount: 60_000_000.00m, Category: MatterCategory.CrossBorder, ConflictWaiverDocumentId: null); // 中小律所未开启强制豁免策略
// 2. 发起请求 var response = await client.PostAsJsonAsync("/api/legal/matters", command);
// 3. 断言成功:普通租户不受 Tier 2(A) 特化策略约束 response.StatusCode.ShouldBe(HttpStatusCode.OK); }
[Fact] public async Task RedCircleTenant_SubmittingHighValueMatter_WithoutWaiver_ShouldBeBlockedByTier2Rule() { // 1. 使用红圈涉外大所租户(租户 1000001)发起同样的 6000 万涉外诉讼 var client = _factory.CreateAuthenticatedClient(tenantId: "1000001", role: "SuperAdmin"); var command = new CreateMatterIntakeCommand( MatterTitle: "跨国离岸投资并购侵权纠纷", ClientName: "某离岸投资基金会", OpposingParty: "某跨国控股财团", ClaimAmount: 60_000_000.00m, Category: MatterCategory.CrossBorder, ConflictWaiverDocumentId: null); // 未上传豁免函
// 2. 发起请求 var response = await client.PostAsJsonAsync("/api/legal/matters", command);
// 3. 断言阻断:Tier 2 策略生效,短路返回 400 Bad Request response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); var result = await response.Content.ReadFromJsonAsync<Result<string>>(); result.ShouldNotBeNull(); result.IsFailure.ShouldBeTrue(); result.Error.Code.ShouldBe(LegalValidationErrors.WaiverRequired.Code); }}4 阶动态校验设计核心收益
- 核心逻辑零污染:通用不变量(Tier 1)与租户特化策略(Tier 2)物理隔离,消除脆弱的
if (tenantId == ...)坏味道; - 渐进式能力追加:基础规则通过后,策略分派器才追加执行高阶策略,保证了最快响应与极佳的短路性能;
- 零发版热适应:通过配置提供者与规则引擎,运营人员在后台修改阈值即可全网生效,真正实现企业级云原生敏捷。