fix(definitions): F05 定义库保存原子化并引入 revision 冲突检测
- FormulaDefinition 增加 revision 字段(迁移 0005) - 保存接口在单事务内锁定用户所属行序列化并发:配额(50/数据集)检查与写入原子化, 不再可能产生第 51 条;唯一约束竞争返回 409 - 同名更新必须携带匹配的 revision,两个窗口不再相互静默覆盖; GET 列表返回当前 revision;保存增加 30/小时限流 - 审计与定义变更(保存/删除)同一事务提交,审计失败一并回滚 - 前端保存同名定义时携带已知 revision,并明示“同名覆盖”行为 - 测试:revision 边界(缺失/过期/正确/再次过期)、配额拒绝与释放、 审计失败回滚;PostgreSQL 并发配额用例独立门禁(本机 SQLite 跳过)
This commit is contained in:
@@ -31,6 +31,7 @@ type FormulaExpression =
|
||||
type SavedFormulaDefinition = {
|
||||
id: string;
|
||||
definition: Record<string, unknown>;
|
||||
revision: number;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
@@ -802,10 +803,18 @@ export function FormulaExportDialog({
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
// 同名保存按服务端匹配规则覆盖旧版本;带上已知版本的 revision,
|
||||
// 其他窗口先更新过则服务端返回 409,避免静默覆盖。
|
||||
const normalizedName = (value: string) => value.trim().toLowerCase();
|
||||
const existing = definitions.find(
|
||||
(item) =>
|
||||
normalizedName(String(item.definition.name)) ===
|
||||
normalizedName(String(column.name)),
|
||||
);
|
||||
const saved = await api<SavedFormulaDefinition>(
|
||||
`/datasets/${ds.id}/formula-definitions`,
|
||||
"POST",
|
||||
{ definition: column },
|
||||
{ definition: column, revision: existing?.revision },
|
||||
);
|
||||
setDefinitions((items) =>
|
||||
[...items.filter((item) => item.id !== saved.id), saved].sort((a, b) =>
|
||||
@@ -914,6 +923,11 @@ export function FormulaExportDialog({
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{definitions.length > 0 && !task && (
|
||||
<p className="muted">
|
||||
保存同名定义会覆盖原版本;若已在其他窗口被修改,保存将提示刷新后重试。
|
||||
</p>
|
||||
)}
|
||||
<label className="check">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
Reference in New Issue
Block a user