Spring Boot 3 + PostgreSQL 场景工程持久化:revision、contentHash 与历史回滚

适用范围

本文对应 LiTwin 今天新增的项目与场景工程持久化功能,技术栈为 Java 17、Spring Boot 3、Spring JDBC、PostgreSQL 和 Flyway。

本次实现解决四个问题:

  • 项目和当前场景在服务重启后仍然保留;
  • 场景保存有 revision 和并发冲突保护;
  • 历史场景可以查询;
  • 回滚通过生成新版本完成,历史快照不被修改。

1. 数据模型

本次新增 Flyway 迁移:

text 复制代码
V6__project_scene_tables.sql

主要包含三类数据:

数据 作用
project 项目名称、描述、创建和更新时间
scene_current 每个项目的当前场景、revision、contentHash 和 JSONB 内容
scene_version 每次有效变更后的完整场景快照

审计继续复用已有的 audit_log,记录项目、revision、变更类型、原因和 contentHash,不记录完整场景 JSON。

2. API 接口

统一前缀为 /api/v1

项目接口

text 复制代码
POST /projects
GET  /projects?page=0&pageSize=20
GET  /projects/{projectId}

创建项目时,服务端同时创建一个空场景,初始 revision=1,并写入 CREATE 版本快照。

当前场景接口

text 复制代码
GET /projects/{projectId}/scene
PUT /projects/{projectId}/scene

保存请求至少包含:

json 复制代码
{
  "expectedRevision": 1,
  "reason": "保存场景布局",
  "content": {}
}

content 必须符合 contracts/scene.schema.jsonexpectedRevision 必须和当前版本一致。

历史和回滚接口

text 复制代码
GET  /projects/{projectId}/scene/versions
GET  /projects/{projectId}/scene/versions/{revision}
POST /projects/{projectId}/scene/rollback

历史列表只返回摘要,完整场景内容通过具体 revision 查询。回滚请求需要同时携带目标版本和当前版本:

json 复制代码
{
  "targetRevision": 1,
  "expectedRevision": 3,
  "reason": "撤回错误的层级调整"
}

3. 版本语义

版本规则不是简单的"每次请求加一"。

创建

创建项目时生成空场景:

text 复制代码
revision 1 / changeType CREATE

有效保存

当前版本为 1,保存一份新内容后:

text 复制代码
revision 2 / changeType UPDATE

保存时会记录变更后的完整快照。

相同内容保存

服务端会计算规范化 JSON 的 SHA-256:

  • 对象键按字典序处理;
  • 空白不会影响 hash;
  • 数组顺序会影响 hash;
  • 数值按稳定格式处理。

如果新的 contentHash 和当前版本一致,则认为是幂等 no-op,不增加 revision,也不写新的版本和变更审计。

回滚

回滚不修改旧版本,而是复制目标历史快照生成新版本:

text 复制代码
revision 1 → revision 2 → revision 3
                         ↘ revision 4 / ROLLBACK

历史表只允许追加,旧版本不会被 UPDATE 或 DELETE。

4. 并发控制

保存和回滚都在事务中执行:

  1. 对当前场景行加 FOR UPDATE
  2. 比较请求中的 expectedRevision
  3. 版本一致时执行条件更新;
  4. 写入历史快照和审计;
  5. 任意一步失败,整个事务回滚。

版本不一致返回:

text 复制代码
409 SCENE_REVISION_CONFLICT

这样可以避免两个页面同时编辑时,后保存的请求直接覆盖前一个人的结果。

5. 场景内容校验

后端使用支持 JSON Schema 2020-12 的校验器,对场景内容执行完整校验。SceneValidator 还补了跨字段语义检查:

  • content.project.id 必须等于路径中的 projectId
  • assetIdnodeIdbindingIdeventIdviewIdboardId 必须唯一;
  • 节点的 assetIdchildren 引用必须存在;
  • 禁止自引用、循环引用和多父节点;
  • 绑定和事件动作引用的节点必须存在;
  • 场景大小上限为 5 MiB;
  • 自由 JSON 最大嵌套深度为 16;
  • 对象键不允许出现 passwordprivateKeysecrettokencredential 等字段。

前端的 validateSceneProject 也补了数量上限、重复 ID、引用关系和环检测,但它只是快速反馈,后端校验才是实际安全边界。

6. 代码入口

本次主要实现文件:

text 复制代码
backend/api-service/src/main/java/com/litwin/api/controller/ProjectController.java
backend/api-service/src/main/java/com/litwin/api/service/ProjectService.java
backend/api-service/src/main/java/com/litwin/api/service/ProjectStore.java
backend/api-service/src/main/java/com/litwin/api/service/SceneValidator.java
backend/api-service/src/main/resources/db/migration/V6__project_scene_tables.sql
frontend/packages/scene-schema/src/validate.ts
contracts/openapi.yaml
contracts/scene.schema.json

项目文档里,docs/tasks/003-项目与场景工程持久化及版本管理.md 是任务边界和验收标准,docs/20-项目与场景工程持久化实现设计.md 是这次实现的设计记录。第一次接手项目时,先看这两份,比直接翻代码省事。

7. 验收记录

当前实现设计文档记录的结果是:

  • Java 测试 31 项通过,0 失败,0 错误;
  • mvn -q clean package -DskipTests 全模块通过;
  • pnpm typecheckpnpm build 通过;
  • 前端 Node 冒烟检查 11/11。

这些是仓库交接文档中的验收记录,不把它扩大成生产环境已经完成部署。Testcontainers 集成测试仍然依赖可用 Docker 环境,生产 PLC、三维编辑器和发布运行时也不在这次任务范围内。

8. 开发方式

这次仍然使用 AI 大模型辅助实现,但规则没有变:先读任务单和契约,再写代码;不擅自扩大范围;实现后必须有测试和审核记录。模型负责提高实现速度,版本语义、数据边界和安全规则不能交给模型临时决定。

相关推荐
涛思数据(TDengine)1 小时前
存储成本降低80%,Zendure用TDengine支撑117万台设备的能源数据分析
大数据·数据库·人工智能·数据分析·时序数据库·tdengine·工业
ms365copilot1 小时前
OneDrive Copilot 从图像获取见解,读懂图表、示意图
人工智能·copilot·onedrive
IT·陈寒1 小时前
Vue的响应式比我想象的更“敏感“
人工智能·大模型·api·创业·变现·简历优化
object not found1 小时前
Nuxt4去掉body中默认的边距
开发语言·后端·rust
我有满天星辰1 小时前
【从 0 打造我的本地 AI 知识库】在 M1 Mac 上搭建 Ollama:我的本地 AI 模型到底应该怎么选?
人工智能·macos
晚安code1 小时前
DeepSeek Flash 系列降价落地:缓存输入 0.02 元、最高降幅 60%
ai编程
Java后端的Ai之路1 小时前
Git pull弹出vim编辑器完整排查指南
开发语言·人工智能·git·编辑器·vim
西瓜拿铁好喝3 小时前
2026 语义缓存实战:把命中契约写进SPEC,MonkeyCode 云端跑通
人工智能·机器学习·缓存
zzzll11113 小时前
Spring Boot 实现数据脱敏:自定义注解 + Jackson 序列化器
java·spring boot·后端