沉淀自超兔一体云 2B 复杂服务合约管理模块的开发现场。前半套是系统分析方法——把一团业务叙述,映射成一份层级分明的数据结构;后半套是单源分片渲染——一份全量数据,多模块按需分片,前端只做渲染不存业务逻辑。
1. 背景:一份 2B 服务合约有多复杂
在 CRM 系统里,订单是一类基础业务对象:客户、产品、数量、金额、日期,一张单据说清一次成交。超兔一体云是一套面向中小微企业的一体化 CRM 系统,客户管理系统、销售管理系统、订单、合同、应收财务跑在同一套数据底座上。服务的这批客户,日常生意就跑在"客户 → 报价 → 订单 → 回款"这条线上——管住这条线,生意就转得起来。
但 2B 业务还有另一头:服务合约。客户买的不是一批货,是一段持续一整年的服务。在超兔CRM 的合同订单管理里,这类业务由服务型合同承载:合约交付计划管"做什么、做到哪",回款计划管"按什么节奏收钱"。概念上说得朴素,真到开发,复杂度才显出来。以一份年度代账服务合约为例:合约金额 48,000 元,拆成 12 个月度期次;每个期次固定一组任务——收集票据、账务处理、报税——每项任务落成执行记录;交付推进到第几期,对应第几笔应收,回款认领跟着期次走;参与交付的同事按期次核算激励;关键节点立里程碑,全年动作铺在日历上。
等到要做"合约详情页"时,问题来了:一个页面要同时呈现合约主体、服务产品、期次、WBS 任务、执行记录、应收、回款、执行费用、激励、里程碑、日历——15 个区块、5 层业务对象、3 条辅线。按惯性做法,前后端要拆 7 个以上接口,每个接口各自拼一份数据;区块之间的数字要对得上,全靠人肉对齐;换一个客户演示,就得重新 mock 一遍。
单源分片渲染方案,就是在超兔一体云开发这类复杂服务合约管理模块的过程中沉淀下来的。它分两半:
- 前一半是系统分析:怎么把一团业务叙述,映射成一份讲得清楚、层级分明的数据结构;
- 后一半是渲染模式:怎么让这一份 JSON 数据,驱动整页 15 个区块,区块之间互不打架。

2. 系统分析方法:业务 → 数据的四步映射
面对复杂业务,常犯的错是拿到需求就开始建表、写接口。这套方法的第一步恰恰相反:先做分拣,再做设计。整个过程四步:捞名词 → 分主线辅线 → 切原子派生 → 区块对分片。这四步合起来,就是一套从业务到数据的映射方案——不止服务合约能用,任何复杂 2B 业务对象都能套。下面逐步拆开。
2.1 第一步:盘点业务对象,把名词全部捞出来
把业务方对这份合约的描述原样记录,圈出所有名词。这个案例圈出来 12 类:合约、客户、服务产品、期次、WBS 任务、执行记录、应收、回款、执行费用、激励、里程碑、日历事件。
此时不要急着画 ER 图——先做分拣。
2.2 第二步:分拣主线与辅线——"删掉它,交付断吗?"
对每个名词问同一个问题:把它从业务里删掉,交付链条断不断?
- 断的,是主线对象:合约 → 服务产品 → 期次 → WBS 任务 → 执行记录。这五层串起来就是"客户买的服务怎么一步步交付出去",抽掉任何一层,交付就讲不清。
- 不断的,是辅线对象:应收/回款/费用描述这场交付的钱,激励描述人,里程碑/日历描述时间。它们不从属于交付链条,而是从钱、人、时间三个维度观察同一场交付。
主线决定数据模型的骨架——五层父子层级;辅线决定血肉——平铺的集合,靠外键挂回主线的某一层。
| 业务名词 | 删掉后交付断吗 | 判定 | 数据归宿 |
|---|---|---|---|
| 合约 / 客户 | 断(没有主体) | 主线 L1 | contract |
| 服务产品 | 断(不知道交付什么) | 主线 L2 | products[] |
| 期次 | 断(不知道何时交付) | 主线 L3 | periods[] |
| WBS 任务 | 断(不知道做什么) | 主线 L4 | wbsTasks[] |
| 执行记录 | 断(不知道做没做) | 主线 L5 | execRecords[] |
| 应收 / 回款 / 执行费用 | 不断(钱在旁边看) | 辅线 · 钱 | receivables[] / collections[] / expenses[] |
| 激励 | 不断(人在旁边看) | 辅线 · 人 | incentives[] |
| 里程碑 / 日历事件 | 不断(时间在旁边看) | 辅线 · 时间 | milestones[] / calendarEvents[] |
5 层模型不是设计出来的,是分拣出来的。
2.3 第三步:切分原子数据与派生数据——"存"还是"算"
对每条要落库的数据问:它有没有独立的业务事实来源?
- 回款单是一笔真实发生的钱 → 原子数据,存储;
- 回款率 = 回款 ÷ 合约金额,是两个事实的比值 → 派生数据,用时计算。
凡是能从既有事实算出来的数字,一律不存。存下来的派生值等于制造第二份真相,两份真相早晚打架。
2.4 第四步:界面区块 ↔ 数据分片,一一映射
界面上每个独立区块对应一个分片(slice):产品卡对应 products[],期次方块对应 periods[],日历对应 calendarEvents[]……派生结果也有自己的分片——KPI 卡、行动中心、交付×回款矩阵都是派生分片。分片之间互不引用,各自只读单源数据。
至此,系统分析的产出物齐了:一份单源 JSON 结构 + 一张分片映射表。剩下的,交给单源分片渲染。
2.5 方法小结:四步一张表,带走就能用
| 步骤 | 动作 | 要问的问题 | 产出物 | 常见反模式 |
|---|---|---|---|---|
| ① 捞名词 | 盘点业务对象 | 业务方的话里出现了哪些名词? | 业务对象清单 | 名词没捞全,直接跳去画 ER 图 |
| ② 分主线辅线 | 逐个对象做删除试验 | 删掉它,交付链条断不断? | 主线层级骨架 + 钱/人/时间辅线 | 把辅线硬塞进主线层级 |
| ③ 切原子派生 | 审每条数据的事实来源 | 它有没有独立的业务事实? | 落库字段清单 + 派生函数清单 | 回款率这类比值也落库 |
| ④ 区块对分片 | 界面区块映射数据分片 | 这个区块吃哪部分数据? | 分片映射表(data-slice 清单) | 一个接口喂多个互不相干的区块 |
四步背后是三条铁律,比步骤本身更值得记:
- 单一真相:全页只认一份单源数据,区块不私拉接口;
- 派生不落库:能从既有事实算出来的,一律不存——存下来的派生值就是第二份真相,两份真相早晚打架;
- 分片互不引用:分片之间不传数据,要联动就各自回单源重算。
这套四步法不挑业务对象。服务合约之外,项目详情、工单详情、采购单详情、设备台账,凡是"一个页面要吃透一个复杂业务对象"的场景都适用。判断标准就一条:这个对象的层级关系和观察维度,多到一张 ER 图画不下、一张嘴说不清——那就值得先坐下来做这四步,再动手建表。
3. 核心思想
一份全量数据,多模块按需分片,前端只做渲染不存业务逻辑。
4. 数据层
4.1 主结构(5 层模型)
{
"contract": { /* 第 1 层:合约主体 */
"id": "C-2026-001",
"title": "上海×××公司 2026 服务合约",
"customer": { "name": "...", "industry": "..." },
"period": { "start": "2026-01-01", "end": "2026-12-31" },
"amount": 48000,
"status": "active"
},
"products": [ /* 第 2 层:服务产品 */
{ "id": "p1", "name": "月度代账服务", "periods": 12, ... }
],
"periods": [ /* 第 3 层:期次 */
{ "id": "p1-1", "productId": "p1", "seq": 1, "status": "done", ... }
],
"wbsTasks": [ /* 第 4 层:WBS 任务 */
{ "id": "w1", "periodId": "p1-1", "name": "收集票据", "status": "done", ... }
],
"execRecords": [ /* 第 5 层:执行记录 */
{ "id": "e1", "wbsId": "w1", "date": "2026-01-03", "content": "..." }
],
"receivables": [ /* 应收 */ ],
"collections": [ /* 回款 */ ],
"expenses": [ /* 执行费用 */ ],
"incentives": [ /* 激励 */ ],
"milestones": [ /* 关键里程碑 */ ],
"calendarEvents": [ /* 日历事件 */ ]
}
4.2 分片映射(slice map)
每个 DOM 区块对应一个分片,约定命名 data-slice="...":
| 分片 key | 数据来源 | 渲染区块 |
|---|---|---|
header |
contract |
Topbar / 客户名 / 合约金额 |
info |
contract |
基本信息四宫格 |
kpi |
派生 | 6 张 KPI 卡(自动算汇总) |
action |
派生 | 行动中心 Top3 |
products |
products[] |
产品执行管控卡片 |
periods |
periods[] |
期次方块 + WBS 任务分解 |
wbs |
wbsTasks[] |
WBS 行 |
exec |
execRecords[] |
执行记录 |
calendar |
calendarEvents[] |
多月视图 + 数据条目 |
matrix |
派生 | 交付×回款关联矩阵 |
ar |
receivables[] |
应收列表 |
rc |
collections[] |
回款列表 |
expense |
expenses[] |
执行费用表 |
incentive |
incentives[] |
激励一体化看板 |
milestone |
milestones[] |
关键里程碑 |
4.3 派生(derived)数据
下列数据不存储,由前端基于基础层派生计算:
kpi:合约总额、回款率、未回款金额、活跃期次等action:Top3 待办(按优先级+截止日期排序)matrix:每期交付/回款配对(关联receivables+periods)team-bar:按人员聚合incentivescalendar.summary:本月计划/交付/应收/实收
后端只提供"原子数据 + 业务规则",前端只做"分片 + 派生 + 渲染"。
5. 渲染层
5.1 模板占位
DOM 中用 data-slice 标记分片容器,用 data-bind 标记值绑定点:
<!-- 容器标记 -->
<div class="hero-card" data-slice="header">
<div class="contract-title" data-bind="contract.title"></div>
<div class="contract-id" data-bind="contract.id"></div>
</div>
<!-- 列表标记(使用 template 元素) -->
<template data-slice="products" data-list="products">
<div class="product-card">
<div class="product-name" data-bind="name"></div>
<div class="product-amount" data-bind="amountText"></div>
</div>
</template>
5.2 绑定语法
| 语法 | 含义 | 示例 |
|---|---|---|
data-bind="a.b.c" |
绑定到 data.a.b.c |
data-bind="contract.id" |
data-bind="a + ' - ' + b" |
字符串拼接 | data-bind="customer.name + ' · ' + contract.id" |
data-bind="amountText" |
渲染前调用 formatAmount() |
|
data-attr-class="status" |
把字段值作为 class 名 | |
data-attr-style="progress: percent%" |
拼装为 inline style |
5.3 派生计算器
const derived = {
kpi: (data) => ({
totalAmount: data.contract.amount,
collectedRate: data.collections.reduce(...) / data.contract.amount
}),
matrix: (data) => periods.map(p => ({
period: p.seq,
delivery: ...,
collection: ...
}))
};
6. 加载流程

降级策略:生产用真接口,演示/原型用本地 JSON。换 JSON 即可展示不同客户、不同合约的进度。
7. 与传统方式的对比
| 维度 | 传统多接口 | 单源分片 |
|---|---|---|
| 接口数 | 7+ | 1 |
| 跨模块数据一致性 | 弱(各自拉取) | 强(一次拉全) |
| 后端聚合成本 | 高(每个接口都要拼) | 低(一次拼好) |
| 派生计算位置 | 后端重复算 | 前端按需算 |
| JSON 切换演示 | 需 mock 多接口 | 换 1 个 JSON 即可 |
| 适合场景 | 微服务 + 极大规模 | 单页面 + 中等规模 |
8. 落地步骤
- 盘点业务对象 — 捞名词、分主线辅线(见第 2 节)
- 设计主结构 — 按 5 层模型 + 辅线集合组织单源 JSON
- 切分原子与派生 — 有事实来源的落库,算得出的写进
derived.js - 标注 DOM — 在 HTML 中加
data-slice/data-bind - 实现渲染器 — 编写
render.js(约 100-200 行) - 配置降级 —
apiUrl与demoUrl切换 - 验证 — 保证 JSON 切换后页面 1:1 还原
9. 复用价值与结语
- 新增产品/合约:复制 JSON 模板,替换数据,DOM 0 改动
- 改业务字段:JSON 加字段 + DOM 加
data-bind即可 - 跨页面复用:相同 JSON 喂给"列表页/详情页/时间线页"
- 跨业务复用:四步映射法换个业务对象照走一遍——项目详情、工单详情、采购单详情,路数一样
- 离线 Demo:纯前端可演示完整业务流程
回头看,这套方案里真正值钱的不是渲染器那一两百行代码,而是前半段的映射分析:先分拣主线辅线,再切分原子派生,最后区块对分片——分析做对了,"7 个互相打架的接口"自然就变成了"1 个讲得清楚的单源"。
这也符合超兔一体云做产品的一贯路径:方法不是先设计再找场景,而是被客户的真实业务磨出来的。从客户中来,回客户中去——订单如此,服务合约如此,沉淀出这套方案的这次开发,也是如此。