Skip to content
bitzorcas
中EN

Recipe

实战:前后端集成与元数据自动充水(Hydration)

深入掌握 BitzOrcas.Modern 前后端集成范式:OpenAPI 零手写 TypeScript SDK 自动生成、IReadModelMetaMapped 页级元数据投影、10 级管道第 10 级字典与关联实体充水与 React 19 最佳实践。

Last updated

在现代企业级管理后台中,前端表格与表单频繁需要展示大量关联名称(例如:ClientId 需展示为客户企业全称、StatusCode 需展示为多语言字典标签、UserId 需展示为经办律师姓名)。 传统开发模式往往面临两难绝境:

  1. 让后端每个查询手写 5~10 个 LEFT JOIN:导致 SQL 极度臃肿、查询性能骤降,且严重击穿了微内核模块的物理自治边界;
  2. 让前端发起 N+1 次异步请求查询字典与详情:导致页面产生剧烈的布局抖动(Layout Shift),并在高并发时打垮后端网关。

BitzOrcas.Modern 确立了“窄数据传输 + 10 级管道第 10 级元数据自动充水(ReadModelDisplayPipelineBehavior)”的黄金架构:

  • 后端纯净查询:Handler 仅查询聚合根的核心字段,不发生任何跨模块跨表的宽表 Join;
  • 管道拦截批量水合:10 级管道的最后一级(第 10 级)统一拦截查询响应,从 Redis 缓存中毫秒级批量补全字典与关联实体的显示文本,作为伴生 meta 挂载在响应外层;
  • 前端零样板消费:基于 @bitz/platform-sdk 与 @bitz/widgets,表格直接通过行级 __meta 投影快速渲染,无需编写任何额外的回显接口。

本指南以法律科技核心业务——**“民商事案件卷宗列表与客户名称/状态自动充水”**为例,带你深入理解前后端集成与元数据水合的底层机制。

前后端元数据充水时序全景

"Redis 7 (:6379 字典与主数据缓存)""10 级管道 (第 10 级 ReadModelDisplay)""BitzOrcas.Api (:6881)""YARP 网关 (:6880)""Redis 7 (:6379 字典与主数据缓存)""10 级管道 (第 10 级 ReadModelDisplay)""BitzOrcas.Api (:6881)""YARP 网关 (:6880)"前端 ServerDataTable 通过 DataTableObjectCell 零延迟同步渲染显示名称"React 19 ServerDataTable"1. GET /api/legal/matters (获取案件分页列表)12. 转发请求至 API Host23. 执行 Query Handler (返回仅含 ClientId, StatusCode 的纯净 DTO 集合)34. ReadModelDisplayPipelineBehavior 拦截 PagedResult<T>45. 提取所有 ClientId 与 Status,并发批量拉取 Redis 字典缓存56. 构造伴生 Meta 分节字典挂入响应体67. 响应带 __meta 投影的完整 JSON7"React 19 ServerDataTable"

第一步:后端 DTO 实现 IReadModelMetaMapped 契约

在查询响应 DTO 上实现 IReadModelMetaMapped 契约,声明该只读模型具备行级 Meta 伴生能力:

src/Modules/Legal/BitzOrcas.Modules.Legal.Contracts/Matters/MatterListItemDto.cs
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 批量解析为显示名称:

src/Modules/Legal/BitzOrcas.Modules.Legal/LegalDisplayMapConfig.cs
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 直接完成元数据渲染,彻底杜绝手工书写字典映射:

frontend/apps/app/src/pages/legal/matters/matter-list-table.tsx
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 享受强类型、无抖动的工业级渲染。

100%

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