沉淀自超兔一体云 2B 复杂服务合约管理模块的开发现场。前半套是系统分析方法——把一团业务叙述,映射成一份层级分明的数据结构;后半套是单源分片渲染——一份全量数据,多模块按需分片,前端只做渲染不存业务逻辑。
后一半是渲染模式:怎么让这一份 JSON 数据,驱动整页 15 个区块,区块之间互不打架。


|
业务名词 |
删掉后交付断吗 |
判定 |
数据归宿 |
|
合约 / 客户 |
断(没有主体) |
主线 L1 |
contract |
|
服务产品 |
断(不知道交付什么) |
主线 L2 |
products[] |
|
期次 |
断(不知道何时交付) |
主线 L3 |
periods[] |
|
WBS 任务 |
断(不知道做什么) |
主线 L4 |
wbsTasks[] |
|
执行记录 |
断(不知道做没做) |
主线 L5 |
execRecords[] |
|
应收 / 回款 / 执行费用 |
不断(钱在旁边看) |
辅线 · 钱 |
receivables[] / collections[] / expenses[] |
|
激励 |
不断(人在旁边看) |
辅线 · 人 |
incentives[] |
|
里程碑 / 日历事件 |
不断(时间在旁边看) |
辅线 · 时间 |
milestones[] / calendarEvents[] |
5 层模型不是设计出来的,是分拣出来的。
凡是能从既有事实算出来的数字,一律不存。存下来的派生值等于制造第二份真相,两份真相早晚打架。
products[],期次方块对应
periods[],日历对应
calendarEvents[]……派生结果也有自己的分片——KPI 卡、行动中心、交付×回款矩阵都是派生分片。分片之间互不引用,各自只读单源数据。|
步骤 |
动作 |
要问的问题 |
产出物 |
常见反模式 |
|
① 捞名词 |
盘点业务对象 |
业务方的话里出现了哪些名词? |
业务对象清单 |
名词没捞全,直接跳去画 ER 图 |
|
② 分主线辅线 |
逐个对象做删除试验 |
删掉它,交付链条断不断? |
主线层级骨架 + 钱/人/时间辅线 |
把辅线硬塞进主线层级 |
|
③ 切原子派生 |
审每条数据的事实来源 |
它有没有独立的业务事实? |
落库字段清单 + 派生函数清单 |
回款率这类比值也落库 |
|
④ 区块对分片 |
界面区块映射数据分片 |
这个区块吃哪部分数据? |
分片映射表(data-slice 清单) |
一个接口喂多个互不相干的区块 |
一份全量数据,多模块按需分片,前端只做渲染不存业务逻辑。

┌─────────────────┐ GET /api/contract/:id/full │ 后端接口 │ ─────────────────────────────────┐ │ (聚合 5 层模型) │ ▼ └─────────────────┘ ┌──────────────────────────────────────┐ │ contract.full.json (单源数据全集) │ └──────────────────────────────────────┘ │ │ │ ┌───────────┘ │ └───────────┐ ▼ ▼ ▼ ┌───────────────┐ ┌────────────────┐ ┌────────────────┐ │ Slices (分片) │ │ Slices │ │ Slices │ │ hero / info │ │ products[] │ │ calendar[] │ │ kpi / action │ │ wbs[] │ │ ar / rc[] │ └───────────────┘ │ periods[] │ │ incentive[] │ └────────────────┘ └────────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────┐ │ DOM (按分片渲染 / 局部刷新) │ └─────────────────────────────────────┘
{
"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": [ /* 日历事件 */ ]
}
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[] |
关键里程碑 |
kpi:合约总额、回款率、未回款金额、活跃期次等action:Top3 待办(按优先级+截止日期排序)matrix:每期交付/回款配对(关联
receivables +
periods)team-bar:按人员聚合
incentivescalendar.summary:本月计划/交付/应收/实收后端只提供"原子数据 + 业务规则",前端只做"分片 + 派生 + 渲染"。
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>
|
语法 |
含义 |
示例 |
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 |
|
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: ...
}))
};

|
维度 |
传统多接口 |
单源分片 |
|
接口数 |
7+ |
1 |
|
跨模块数据一致性 |
弱(各自拉取) |
强(一次拉全) |
|
后端聚合成本 |
高(每个接口都要拼) |
低(一次拼好) |
|
派生计算位置 |
后端重复算 |
前端按需算 |
|
JSON 切换演示 |
需 mock 多接口 |
换 1 个 JSON 即可 |
|
适合场景 |
微服务 + 极大规模 |
单页面 + 中等规模 |
derived.jsdata-slice /
data-bindrender.js(约 100-200 行)apiUrl 与
demoUrl 切换data-bind 即可
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70046958/viewspace-3180370/ ,如需转载,请注明出处,否则将追究法律责任。