Skip to content

案例:工程项目报价单 ​

工程报价单是迄今最复杂的案例——它同时包含深层 DAG 计算链和两对真实的双向纠缠。纯 DAG 无法表达"改报价反推利润率"或"改工期自动重算团队规模"这类对称约束;胶水代码需要手写 6+ 个互相监听的 watcher 且容易产生死循环。MeshForm 引擎原生支持两种拓扑,几行配置搞定。

规模:7 大板块 · 53 个节点 · 42 条 DAG 规则 · 2 对 useEntangle 双向纠缠

板块节点数性质
项目基础信息8用户选择 → 费率/税率推导
WBS 工作分解169 个人天录入 → 7 个阶段小计(DAG)
成本汇总66 层级联汇总(DAG)
报价计算7DAG 建议报价 + ⟳ 利润率 ↔ 最终报价纠缠
资源规划5⟳ 工期 ↔ 团队规模纠缠 + DAG 效率指标
付款方案4含税总价 → 分期金额(DAG)
风险预警3条件显隐(DAG hidden 规则)

实时演示 ​

⟳ 标记的字段(利润率、最终报价、项目工期、团队规模)支持双向编辑——修改任意一个,另一个自动收敛。

依赖关系图 ​

两对纠缠的业务逻辑 ​

纠缠 ①:利润率 ↔ 最终报价 ​

业务场景:成本已知,销售侧有两种工作方式:

  • 内部报价模式:设定利润率(如 20%),系统自动算出建议报价
  • 客户预算模式:客户说"预算只有 50 万",直接输入报价,系统反推实际利润率
成本(DAG 固定值)
   × (1 + 利润率%) = 建议报价(DAG,只读参考)

          ↕ ⟳ useEntangle(双向)

用户改 利润率  → 最终报价更新(按公式)
用户改 最终报价 → 利润率更新(反算 = 报价/成本 - 1)
typescript
// 建议报价(DAG,随成本和利润率联动,只读参考值)
'pricing.suggestedQuote.value': from(
  ['costs.totalCost', 'pricing.marginRate'],
  (tc, mr) => Math.round(tc * (1 + mr / 100))
),

// 纠缠 A:利润率 → 最终报价("我要赚 20%" → 算报价)
// isProxy:true 让 emit 参数是代理对象,直接 .value 读取状态
// propose.set 是提交变更的唯一正确方式(同步 emit 的 return 值会被引擎忽略)
engine.entangle({
  cause: 'pricing.marginRate',
  impact: 'pricing.quotePrice',
  via: ['value'],
  isProxy: true,
  emit: (mrNode, qpNode, propose) => {
    const tc = totalCostCache.value   // 从 onFormChange 拿到最新成本
    if (tc <= 0) return
    const target = Math.round(tc * (1 + mrNode.value / 100))
    if (Math.abs(target - qpNode.value) < 1) return
    propose.set('value', target)
  },
})

// 纠缠 B:最终报价 → 利润率("客户预算 50 万" → 反推利润率)
engine.entangle({
  cause: 'pricing.quotePrice',
  impact: 'pricing.marginRate',
  via: ['value'],
  isProxy: true,
  emit: (qpNode, mrNode, propose) => {
    const tc = totalCostCache.value
    if (tc <= 0) return
    const target = Math.round((qpNode.value / tc - 1) * 100 * 10) / 10
    if (Math.abs(target - mrNode.value) < 0.1) return
    propose.set('value', target)
  },
})

纠缠 ②:工期 ↔ 团队规模 ​

业务场景:WBS 算出总人天是固定的,资源规划侧有两种思路:

  • 固定工期模式:工期定 90 天,系统算出最少需要几个人
  • 固定团队模式:只有 3 个人可用,系统算出完工需要多少天
总人天(DAG 固定值)= 工期 × 团队规模 × 并行效率

          ↕ ⟳ useEntangle(双向)

用户改 工期   → 团队规模更新(总人天 ÷ 工期 ÷ 效率)
用户改 团队规模 → 工期更新(总人天 ÷ 人数 ÷ 效率)
typescript
// 纠缠 C:工期 → 团队规模("90 天交付,需要几个人?")
engine.entangle({
  cause: 'resource.duration',
  impact: 'resource.teamSize',
  via: ['value'],
  isProxy: true,
  emit: (durNode, tsNode, propose) => {
    const pd  = personDaysCache.value
    const eff = efficiencyCache.value || 0.8
    if (pd <= 0 || durNode.value <= 0) return
    const target = Math.ceil(pd / durNode.value / eff)
    if (Math.abs(target - tsNode.value) < 0.01) return
    propose.set('value', target)
  },
})

// 纠缠 D:团队规模 → 工期("只有 3 个人,要做多久?")
engine.entangle({
  cause: 'resource.teamSize',
  impact: 'resource.duration',
  via: ['value'],
  isProxy: true,
  emit: (tsNode, durNode, propose) => {
    const pd  = personDaysCache.value
    const eff = efficiencyCache.value || 0.8
    if (pd <= 0 || tsNode.value <= 0) return
    const target = Math.ceil(pd / tsNode.value / eff)
    if (Math.abs(target - durNode.value) < 0.01) return
    propose.set('value', target)
  },
})

为什么这些场景纯 DAG 做不了? ​

DAG 要求每个节点有固定的"上游"和"下游"。

  • 利润率 → 报价:DAG 可以
  • 报价 → 利润率:DAG 不行,这是同一对关系的反向

如果强行用 DAG 表达:报价 既依赖 利润率(正向),又被 利润率 依赖(反向),形成自环——DAG 定义不允许这种结构。

用 watch 手写:改报价触发 watch → 更新利润率 → 触发利润率的 watch → 更新报价 → 死循环。必须手动加 isUpdating flag,且 4 对关系需要写 8 个带 flag 的 watcher。useEntangle 用"提案-收敛"机制天然避免死锁,无需 flag。

DAG 主链概览 ​

项目类型 ──┬──→ PM/开发/QA 日费率 ──┐
          └──→ 管理费率/风险率       │
                                    ↓
客户行业 ─────────→ 增值税率    WBS 人天录入(9 个)
                        │            │
                        │       ┌────┘
                        │       ↓
                        │   各阶段人天 × 费率 = 阶段成本(3 阶段)
                        │       │
                        │       ↓
                        │   总人力成本
                        │       │
                        │   管理费(× 管理费率)    ← 管理费率
                        │       │
                        │   风险储备金(× 风险率)  ← 风险率
                        │   + 硬件采购 + 差旅
                        │       │
                        │       ↓
                        │   项目总成本
                        │       │
                        │       ↓
                        │   ⟳ 纠缠①(利润率 ↔ 最终报价)
                        │       │
                        └──────→│ 增值税额 = 报价 × 税率
                                │
                                ↓
                            含税总价
                                │
                        ┌───────┴────────┐
                        ↓               ↓
                    签约首付       里程碑中期款
                        └───────┬────────┘
                                ↓
                            验收尾款 = 总价 - 首付 - 中期款

DAG 与纠缠混用说明 ​

三种 API 在同一个 engine 上协作:

typescript
// ① 大多数字段用 from() 语法糖(DAG)
const rules = {
  'wbs.phase1Cost.value': from(
    ['wbs.pm1', 'wbs.dev1', 'wbs.qa1', 'project.pmRate', ...],
    (pm, dev, qa, pmR, ...) => pm * pmR + dev * devR + qa * qaR
  ),
  // ... 共 42 条
}

// ② 对称约束用 engine.entangle()(有环图)
onMounted(() => {
  engine.entangle({ cause: 'pricing.marginRate', impact: 'pricing.quotePrice', ... })
  engine.entangle({ cause: 'pricing.quotePrice', impact: 'pricing.marginRate', ... })
  engine.entangle({ cause: 'resource.duration',  impact: 'resource.teamSize',  ... })
  engine.entangle({ cause: 'resource.teamSize',  impact: 'resource.duration',  ... })
})

MeshFlow 引擎内部维护两套调度机制:DAG 节点走水位线拓扑排序,纠缠节点走"提案-收敛"的纪元演化。两者在同一个 engine.hooks.onSuccess 周期内协调完成,对上层 Vue 渲染层完全透明。

MeshForm Vue - deterministic state orchestration for JSON Forms