“标题字号改成 20,能耗换成平均功率。”两句话都像改配置,影响却不同。字号改变外观,指标替换还会改变单位、聚合和坐标轴。让模型直接改整份页面,容易把这些区别藏起来。
我的方案让模型提出业务命令,编辑器负责定位、校验和保存。命令进入原来的版本与历史机制,用户能够查看差异,也能按明确规则撤销。
对象与版本固定在发起请求的那一刻
组件使用稳定标识,不用数组下标作为长期引用。排序、删除和同时编辑都会改变下标。选区包含页面、组件和 revision,用户随后点了另一张图,也不改变已经提出的目标。
revision 由数据库版本列管理,document 保存组件内容,不在两处维护相同的版本。模型不能在参数里声明身份,实际用户和租户由服务端确定。
命令表达业务动作,不暴露任意写入口
最小范围可以支持 set-title-size 和 replace-metric。二者共用命令标识与预期版本,参数分别检查:
{
"commandId": "command-demo-01",
"pageId": "page-demo",
"expectedRevision": 12,
"operations": [
{"kind": "set-title-size", "targetId": "chart-a", "value": 20},
{"kind": "replace-metric", "targetId": "chart-a", "metricId": "power-average"}
]
}结构校验检查字段和类型,动作处理器检查组件是否支持该属性,业务授权检查当前用户是否能修改。合法 JSON 只通过第一项。
指标替换还要从可信指标目录读取单位、粒度与聚合规则。能耗采用的 sum 不能直接沿用到平均功率,单位也从 kWh 变为 kW。口径有多个候选时由用户选择,不按名称取第一项。
业务命令与存储补丁分开,编辑器可以改变内部结构。模型只需要知道允许的动作,不能通过任意路径顺手修改发布状态或凭证。
预览与提交之间允许发生冲突
预览在固定版本上计算候选文档,显示字号、指标、单位和聚合的变化。涉及数据口径的改动,需要用户确认。确认绑定用户、页面、原版本与规范化命令摘要,实际提交的内容必须相同。
预览不持有长事务。等待确认期间别人可能已修改页面,提交时仍检查预期 revision。条件不符就返回冲突并重新预览,不能无条件覆盖。
页面更新与命令历史在同一事务中保存,命令标识在租户和页面范围内唯一。同一标识、同一摘要的重试返回原结果,换了内容则拒绝。使用 If-Match 时按条件请求语义返回 412,业务版本号方案也可以约定 409,前端不把它们当作网络错误自动重做。
修改在候选副本中完成
from copy import deepcopy
def prepare_change(document, operations, metric_catalog):
candidate = deepcopy(document)
touched = []
for op in operations:
component = candidate["components"].get(op["targetId"])
if component is None:
raise ValueError("component not found")
if op["kind"] == "set-title-size":
value = op["value"]
if type(value) is not int or not 8 <= value <= 72:
raise ValueError("invalid title size")
component["title"]["fontSize"] = value
elif op["kind"] == "replace-metric":
metric = metric_catalog[op["metricId"]]
if component["type"] not in metric["supportedComponents"]:
raise ValueError("incompatible metric")
component["binding"] = {
"metric": op["metricId"], "unit": metric["unit"],
"aggregation": metric["defaultAggregation"]
}
else:
raise ValueError("unsupported operation")
touched.append(op["targetId"])
return candidate, sorted(set(touched))这个函数假设结构和身份已检查、指标目录可信。动作只接触处理器允许的字段,任何一步失败都不改变原文档。候选还要通过整页约束,然后才参与条件保存。
字号和指标都在一份页面文档中,可以整体成功或失败。跨外部系统的动作不能靠包装函数获得原子性;例如创建数据源后绑定页面,需要临时资源、失败清理和待处理记录,不能隐瞒部分完成。
读回、渲染与撤销是不同结果
提交返回新版本和变化摘要,界面更新该版本。读回关键字段确认保存,图表查询与渲染确认绑定可用。保存成功不替代后两项。
撤销只针对本次改动的字段生成反向命令,不把整页旧快照盖回去。字段仍等于本次新值时,可以在最新版本上预览撤销;别人已改同字段,就报告冲突。其他组件的修改不应被顺带恢复。
验收包括并发同版本最多一次提交、响应丢失后重试不多写历史、指标失败不保存字号、确认内容变化被拒绝,以及撤销不覆盖他人后续修改。命令、版本、历史与实际界面保持对应,用户才能知道一句话真正改了什么。