在现代企业级管理后台中,前端表格与表单频繁需要展示大量关联名称(例如:ClientId 需展示为客户企业全称、StatusCode 需展示为多语言字典标签、UserId 需展示为经办律师姓名)。
传统开发模式往往面临两难绝境:
- 让后端每个查询手写 5~10 个
LEFT JOIN:导致 SQL 极度臃肿、查询性能骤降,且严重击穿了微内核模块的物理自治边界; - 让前端发起 N+1 次异步请求查询字典与详情:导致页面产生剧烈的布局抖动(Layout Shift),并在高并发时打垮后端网关。
BitzOrcas.Modern 确立了“窄数据传输 + 10 级管道第 10 级元数据自动充水(ReadModelDisplayPipelineBehavior)”的黄金架构:
- 后端纯净查询:Handler 仅查询聚合根的核心字段,不发生任何跨模块跨表的宽表 Join;
- 管道拦截批量水合:10 级管道的最后一级(第 10 级)统一拦截查询响应,从 Redis 缓存中毫秒级批量补全字典与关联实体的显示文本,作为伴生
meta挂载在响应外层; - 前端零样板消费:基于
@bitz/platform-sdk与@bitz/widgets,表格直接通过行级__meta投影快速渲染,无需编写任何额外的回显接口。
本指南以法律科技核心业务——**“民商事案件卷宗列表与客户名称/状态自动充水”**为例,带你深入理解前后端集成与元数据水合的底层机制。
前后端元数据充水时序全景
第一步:后端 DTO 实现 IReadModelMetaMapped 契约
在查询响应 DTO 上实现 IReadModelMetaMapped 契约,声明该只读模型具备行级 Meta 伴生能力:
using System.Collections.Generic;using BitzOrcas.Application.Abstractions.Queries;using BitzOrcas.Domain.Results;
namespace BitzOrcas.Modules.Legal.Contracts.Matters;
/// <summary>/// 案件列表只读查询数据传输对象/// </summary>/// <remarks>/// <para>紧凑承载列表核心字段,禁止在底层 SQL 中强行 JOIN 客户表或字典表。</para>/// <para>实现 <see cref="IReadModelMetaMapped"/>,由 10 级管道第 10 级自动注入行级伴生 Meta。</para>/// </remarks>public sealed record MatterListItemDto : IReadModelMetaMapped{ /// <summary> /// 案件唯一主键 /// </summary> public string Id { get; init; } = string.Empty;
/// <summary> /// 案件业务流水号 /// </summary> public string MatterCode { get; init; } = string.Empty;
/// <summary> /// 案件标题 /// </summary> public string Title { get; init; } = string.Empty;
/// <summary> /// 委托客户唯一标识符 /// </summary> public string ClientId { get; init; } = string.Empty;
/// <summary> /// 承办主审律师标识符 /// </summary> public string LeadLawyerId { get; init; } = string.Empty;
/// <summary> /// 案件状态编码 (1=Draft, 2=PendingReview, 3=Active, 4=Closed) /// </summary> public string Status { get; init; } = string.Empty;
/// <summary> /// 诉讼标的金额 (元) /// </summary> public decimal ClaimAmount { get; init; }
/// <summary> /// 行级伴生实体与字典映射容器 (由管道自动填充,业务代码无需赋值) /// </summary> public IReadOnlyDictionary<string, IReadOnlyDictionary<string, EntityRef>>? Meta { get; init; }
/// <summary> /// 实现契约:写回携带行级元数据的新记录副本 /// </summary> object IReadModelMetaMapped.WithMeta( IReadOnlyDictionary<string, IReadOnlyDictionary<string, EntityRef>> meta) => this with { Meta = meta };}第二步:配置模块级 DisplayMap 映射规则
在业务模块装配中注册充水规则,告知管道如何将 DTO 中的外键 ID 批量解析为显示名称:
using BitzOrcas.Application.DisplayMap;using BitzOrcas.Modules.Legal.Contracts.Matters;using Microsoft.Extensions.DependencyInjection;
namespace BitzOrcas.Modules.Legal;
/// <summary>/// 案件模块元数据充水映射规则注册器/// </summary>public static class LegalDisplayMapConfig{ /// <summary> /// 注册案件元数据充水映射关系 /// </summary> /// <param name="services">DI 服务集合。</param> public static IServiceCollection AddLegalDisplayMaps(this IServiceCollection services) { services.AddReadModelDisplayMap<MatterListItemDto>(map => { // 1. 将 ClientId 自动充水为客户主数据中的全称与统一信用代码 map.MapField(x => x.ClientId) .FromProvider("ClientMasterData", key => $"MasterData:Client:{key}");
// 2. 将 LeadLawyerId 自动充水为人员姓名与执业律所 map.MapField(x => x.LeadLawyerId) .FromProvider("UserProfile", key => $"Identity:User:{key}");
// 3. 将 Status 状态枚举充水为多语言字典文本 map.MapDictionary(x => x.Status) .FromCategory("Legal.MatterStatus"); });
return services; }}第三步:前端 React 19 消费充水元数据
后端返回的标准 JSON 载荷如下所示,外层或行内自动携带 __meta 投影:
{ "items": [ { "id": "MAT-2026-0001", "matterCode": "CIV-2026-0081", "title": "跨国半导体专利侵权诉讼案", "clientId": "CLI-88902", "leadLawyerId": "USR-1002", "status": "Active", "claimAmount": 58000000.00, "__meta": { "clientName": "芯原微电子(上海)股份有限公司", "clientUnifiedSocialCreditCode": "9131000076210088X", "leadLawyerName": "张晓明合伙人", "statusDisplayText": "正式立案在审" } } ], "pageIndex": 1, "pageSize": 20, "totalCount": 1}前端基于 @bitz/widgets 的 ServerDataTable 与 DataTableObjectCell 直接完成元数据渲染,彻底杜绝手工书写字典映射:
import { type JSX } from 'react';import { DataTableObjectCell, ServerDataTable, StatusBadge, type DataTableColumn,} from '@bitz/widgets';import type { LegalMatterSummaryDto } from '@bitz/platform-sdk';
/** * 案件列表渲染列配置 */export const legalMatterColumns: DataTableColumn<LegalMatterSummaryDto>[] = [ { id: 'matterCode', header: '案件流水号', cell: (row) => ( <span className="font-mono text-sm font-semibold tracking-tight"> {row.matterCode} </span> ), width: '180px', }, { id: 'title', header: '案件标题', cell: (row) => ( <span className="font-medium text-neutral-900 dark:text-neutral-100"> {row.title} </span> ), }, { id: 'client', header: '委托人 / 客户', cell: (row) => ( // 核心:从行内 __meta 快速读取充水后的客户全称与信用代码,回退显示原始 ClientId <DataTableObjectCell primaryText={row.__meta?.clientName ?? row.clientId} secondaryText={row.__meta?.clientUnifiedSocialCreditCode} /> ), width: '240px', }, { id: 'leadLawyer', header: '承办主办律师', cell: (row) => ( <span className="text-sm text-neutral-700 dark:text-neutral-300"> {row.__meta?.leadLawyerName ?? row.leadLawyerId} </span> ), width: '140px', }, { id: 'status', header: '状态', cell: (row) => ( <StatusBadge tone={row.status === 'Active' ? 'success' : 'neutral'} label={row.__meta?.statusDisplayText ?? row.status} /> ), width: '130px', },];总结
元数据自动充水架构为现代前后端协同带来了革命性的工程红利:
- 查询性能与架构解耦兼得:后端 SQL 保持纯粹,无需跨模块跨表
LEFT JOIN,单表索引覆盖性能提升 3~5 倍; - 批量缓存极致吞吐:10 级管道在内存和 Redis 中对主数据键进行统一去重和批量提取,网络往返降至 1 次;
- 前端开发极简舒适:前端组件零手写字典解析函数,直接通过
__meta享受强类型、无抖动的工业级渲染。