diff --git a/README.md b/README.md index 474ee44..7ee8eea 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,33 @@ Adminive 使用 C++20 模板、Concepts 和可替换适配器描述业务类型。同一份后端描述驱动 JSON 双向转换、事务式赋值、前端可编辑性、校验、amis CRUD、枚举列表列、树状配置、联动条件、日期控件、颜色控件和一次性提交。属性能力、Schema 与同步拓扑由 `third_party/Structive` 提供,Adminive 只叠加后台管理领域的描述、JSON、HTTP、AMIS 和事务适配。核心层不依赖具体 JSON 库、枚举反射库或结构体反射库。前端只读取 `/admin/amis` 并渲染后端返回的页面 JSON,不包含业务字段或业务判断。 +## 设计理念 + +Adminive 的核心定位是:**让后端已有的 C++ 结构直接驱动后台管理能力,而不是再建立一套独立的数据模型。** + +整体职责分为两层: + +```text +Structive + 负责 Property、Schema、intrinsic read/write 与 synchronization + +Adminive + 负责后台领域 metadata、JSON、HTTP、AMIS、managed transaction 与 frontend projection +``` + +当前设计遵守几个核心原则: + +- Structive 管“对象本身是什么”,Adminive 管“后台系统怎样消费它”。 +- 字段默认 intrinsic read-only,只有 `.editable()`、`.creatable()` 或 `.read_write()` 显式进入 managed mutation path。 +- `read_only` 是能够影响真实运行成本的结构信息:只读存储属性不为自身贡献 lock slot/mutex,managed read 走零锁路径。 +- `editable`、`sensitive`、`widget` 等是 Adminive 领域 metadata,不是通用 Property 访问控制。外部系统是否暴露字段由外部系统自己决定。 +- Synchronization 只负责进程内 mutable consistency;`Resource_Transaction` 负责外部副作用的 prepare/commit/rollback;数据库事务继续由数据库负责。 +- JSON、HTTP、枚举反射和结构体反射都通过 Adapter 接入,第三方依赖不进入 Adminive Core 协议。 +- 前端只消费后端描述,不维护第二份业务 Schema。 +- 静态可知的能力尽量在编译期表达,动态输入才进入 runtime adapter。 + +完整的设计理念、职责边界、同步原则、事务分层和 API 演进规则见 [backend/library/DESIGN.md](backend/library/DESIGN.md)。 + ## 后端分层 `backend/library` 是库级协议层,只有头文件,不依赖也不链接任何具体 JSON、HTTP Server、枚举反射或结构体反射实现。它依赖项目内的 `third_party/Structive`,由 Structive 负责 intrinsic property capability、Schema 和 synchronization;Adminive 自身包含 descriptor、JSON 适配协议、AMIS schema、状态协议、managed adapter 和抽象 `Resource_Service`: diff --git a/backend/library/DESIGN.md b/backend/library/DESIGN.md new file mode 100644 index 0000000..ebadb0a --- /dev/null +++ b/backend/library/DESIGN.md @@ -0,0 +1,859 @@ +# Adminive 设计理念与架构原则 + +本文描述 Adminive 当前架构的设计目标、职责边界和长期演进原则。它不是 API 速查表;具体接口和完整示例仍以项目根目录 `README.md` 为准。 + +## 1. 项目定位 + +Adminive 的目标不是重新发明对象模型、反射系统或属性系统,而是把后端已经存在的 C++ 结构投影成后台管理能力。 + +可以把两层职责概括为: + +```text +Structive + 描述并管理“这个 C++ 对象本身是什么” + +Adminive + 描述“后台管理系统应该怎样展示、编辑、传输和提交这个对象” +``` + +因此 Adminive 更接近一个 backend-driven administration projection layer,而不是一个新的 Property Core。 + +它围绕同一份后端描述连接这些领域: + +```text +C++ business type + │ + ▼ +Adminive descriptor + │ + ├── JSON encode / decode + ├── frontend schema + ├── AMIS CRUD / form / table + ├── HTTP resource service + ├── validation / error path + ├── managed update + └── external transaction +``` + +前端消费后端生成的描述结果,而不是重新维护一份业务字段定义。 + +## 2. 最核心的设计原则 + +Adminive 的核心原则可以浓缩为以下几条: + +1. **Structive 管结构事实,Adminive 管后台管理语义。** +2. **属性信息必须能够影响真实行为,而不只是生成 UI 文本。** +3. **默认只读,显式声明可写。** +4. **同步只保护 mutable consistency,不承担权限、事务或持久化职责。** +5. **JSON、HTTP、枚举反射、结构体反射都是 Adapter,不进入核心协议。** +6. **外部系统是否暴露字段由外部系统自己决定,Property Core 不做访问控制。** +7. **静态可知的事实尽量在编译期表达,动态输入才留到运行期处理。** +8. **前端是后端描述的消费者,不成为第二个业务规则中心。** +9. **外部副作用事务与内存对象同步保持正交。** +10. **不为了统一形式制造重复抽象;已经由 Structive 解决的问题不在 Adminive 再实现一次。** + +后续新增功能如果违反其中任意一条,都应该先重新确认它是否真的属于 Adminive Core。 + +## 3. Structive 与 Adminive 的边界 + +### 3.1 Structive 负责什么 + +`third_party/Structive` 负责对象自身的结构能力: + +```text +Property intrinsic capability +Schema +read / write +read-only optimization +synchronization topology +lock slot +mutex storage +guard +runtime property access +constraint / generic attribute infrastructure +``` + +这些能力与 HTTP、JSON、AMIS、数据库、GUI 没有关系。 + +例如一个 Property 被声明为 intrinsic read-only,Structive 可以直接利用这个事实: + +```text +read_only + ↓ +managed write 不存在 + ↓ +不进入 writable synchronization topology + ↓ +不贡献 lock slot + ↓ +不创建对应 mutex + ↓ +managed read 走零锁路径 +``` + +这类优化属于结构本身,因此应由 Structive 完成。 + +### 3.2 Adminive 负责什么 + +Adminive 负责后台管理领域信息和适配: + +```text +字段名称与显示名称 +editable / creatable +sensitive +required +widget +visible / visible_on +list column metadata +排序信息 +JSON 描述协议 +AMIS schema +HTTP resource semantics +frontend patch +Object_Adapter +Resource_Transaction +``` + +这些信息表达的是“后台系统怎样消费一个结构”,而不是改变 C++ Property 的基本定义。 + +### 3.3 不允许再次出现第二套 Property Core + +Adminive 不应该重新拥有: + +```text +另一套 Property Descriptor +另一套 per-field mutex tree +另一套 lock slot +另一套 managed access mode +另一套 runtime type erasure core +``` + +如果某个需求本质上属于 Property/Schema/synchronization,应优先落到 Structive;如果它属于后台管理领域,则留在 Adminive。 + +这种分层避免两个库同时维护相似概念并逐渐产生不同语义。 + +## 4. “结构事实”与“后台元数据”必须分开 + +Adminive 字段同时包含两类信息,但这两类信息不能混为一谈。 + +第一类是结构事实: + +```text +这个字段在 managed model 中是否 writable +这个字段是否参与 synchronization +字段值的真实 C++ 类型 +字段 accessor +``` + +第二类是后台领域元数据: + +```text +是否允许前端编辑 +是否允许创建时输入 +是否在列表中展示 +是否敏感 +使用什么 widget +显示什么 label +``` + +例如: + +```cpp +ADMINIVE_FIELD(Config, backend_value).read_write() +``` + +表示 `backend_value` 在 Adminive managed model 中可以被修改,但并没有因此自动成为前端可编辑字段。 + +而: + +```cpp +ADMINIVE_FIELD(Config, frontend_value).editable() +``` + +既使它成为 managed writable,也附加“前端 update 可以编辑”的领域语义。 + +因此: + +```text +managed writable + ≠ frontend editable +``` + +这条边界必须长期保持。 + +## 5. 默认只读,显式声明可写 + +Adminive 字段默认是 intrinsic read-only。 + +这是刻意选择,而不是缺省值碰巧如此。 + +理由有三点: + +### 5.1 安全的默认语义 + +后台描述中出现一个字段,并不意味着 HTTP/GUI 更新应该天然拥有修改它的能力。 + +新增字段时默认只读,可以避免因为“忘记配置”而无意进入 managed mutation path。 + +### 5.2 让 metadata 真正参与性能优化 + +只读信息会一路传递到 Structive,同步系统因此可以跳过无意义的锁资源。 + +这比“所有字段都建 mutex,只是在 write 时检查权限”更符合结构描述系统的价值。 + +### 5.3 writable 是结构承诺 + +一旦调用: + +```cpp +.editable() +.creatable() +.read_write() +``` + +就意味着 Adminive 正式承认这个字段存在 managed mutation path。 + +同步、事务和适配器可以据此建立正确行为。 + +## 6. 四种字段声明语义 + +### 6.1 普通字段 + +```cpp +ADMINIVE_FIELD(Config, id) +``` + +语义: + +```text +intrinsic read-only +不产生 managed write +不为自身贡献 Structive mutex +``` + +它仍然是普通 C++ 成员。直接修改 raw member 属于 raw C++ path,不属于 Adminive/Structive managed contract。 + +### 6.2 `.editable()` + +```cpp +ADMINIVE_FIELD(Config, worker_count).editable() +``` + +语义: + +```text +intrinsic read-write +frontend update editable +默认参加 Structive synchronization +``` + +这是后台表单最常见的可编辑字段。 + +### 6.3 `.creatable()` + +```cpp +ADMINIVE_FIELD(Device, address).creatable() +``` + +语义: + +```text +intrinsic read-write +允许 create input +默认参加 Structive synchronization +``` + +它表达创建流程的领域能力,不等同于普通 update editable。 + +### 6.4 `.read_write()` + +```cpp +ADMINIVE_FIELD(Config, backend_state).read_write() +``` + +语义: + +```text +intrinsic read-write +不因为这一声明自动开放 frontend edit +默认参加 Structive synchronization +``` + +适合配置加载、后台代码、服务内部更新等受控路径。 + +### 6.5 `.unsynchronized()` + +```cpp +ADMINIVE_FIELD(Config, atomic_counter).read_write().unsynchronized() +``` + +语义: + +```text +intrinsic writable +不为该路径创建 Structive mutex +``` + +只有以下情况适合使用: + +```text +字段自身已经是原子类型 +更高层已经提供同步 +对象生命周期保证单线程 +或业务明确接受无同步访问 +``` + +`.unsynchronized()` 不是性能开关,更不是“我觉得这里应该没事”。它是对同步责任的显式转移。 + +## 7. Adminive 不做访问控制系统 + +Adminive 中的 `editable`、`readable`、`sensitive` 等信息服务于具体后台适配语义,但它们不构成一个通用安全边界。 + +Structive 更不会定义: + +```text +internal +external +persistence +RPC +GUI +admin +user +``` + +这类访问角色。 + +原因是不同消费者拥有不同边界: + +```text +GUI +HTTP API +RPC +plugin +script +debug tooling +persistence +``` + +是否向某个消费者暴露字段,应由那个消费者或更高层业务策略决定。 + +因此 Adminive 的设计原则是: + +> Property 描述自身能做什么;外部系统决定自己允许做什么。 + +`sensitive()` 可以指导 Adminive JSON/HTTP 等适配器避免泄漏敏感内容,但不应被误解为 C++ 内存级权限机制。 + +## 8. Managed path 与 raw C++ path + +Adminive 不试图取代普通 C++ 对象。 + +例如: + +```cpp +config.worker_count = 8; +``` + +如果成员本身允许直接访问,这仍然是合法 C++。 + +而: + +```cpp +managed.member<&Config::worker_count>().write([](auto& value) { + value = 8; +}); +``` + +属于 managed path。 + +managed path 才承诺: + +```text +遵守 intrinsic writable +遵守 Structive synchronization +维持回调生命周期边界 +与 Resource_Service 的事务流程协作 +``` + +直接绕过 managed API 修改 raw member,也同时绕过这些保证。 + +这符合 Adminive/Structive 的共同原则: + +> 增强原生 C++ 结构,而不是建立一堵无法绕过的对象墙。 + +## 9. 为什么 Managed 回调不能泄漏引用 + +Managed read/write 的锁只在回调作用域中有效。 + +如果允许: + +```cpp +const auto& value = field.read([](const auto& current) -> const auto& { + return current; +}); +``` + +那么引用在返回后仍存在,但锁已经释放,同步契约就失效了。 + +因此 Adminive 在编译期拒绝这些回调结果: + +```text +reference +pointer +reference_wrapper +ranges view +``` + +需要把结果带出 managed scope 时,应: + +```text +返回值副本 +或使用 snapshot() +``` + +这不是 API 限制,而是锁生命周期本身决定的安全边界。 + +## 10. 同步模型:只保护 mutable consistency + +Structive synchronization 只解决一个问题: + +> 当前进程内,对可变对象状态进行 managed access 时如何保持一致性。 + +它不负责: + +```text +HTTP 权限 +数据库事务 +配置文件原子替换 +业务 validator +外部设备写入 +前端确认流程 +``` + +默认情况下,可写属性可以拥有独立同步域,以保留并发性。 + +当一个具体对象实例确实要求跨多个属性的一致快照时,可以使用 shared topology,例如 `Config_Store` 对完整根配置的持久化场景。 + +这种选择应该由一致性需求驱动,而不是为了“简单”把所有对象默认放到一把全局锁下。 + +## 11. 嵌套结构的同步原则 + +嵌套结构不能简单采用“父对象只要有一个 writable 子字段,所有后代访问都锁父对象”的粗粒度规则。 + +Adminive/Structive 应尽量利用真实 intrinsic information: + +```text +只读叶子 + → 不应因为 writable sibling 而产生无意义锁 + +unsynchronized writable leaf + → 不应因为同级字段可写而自动获取无关父锁 + +可整体替换的 writable ancestor + → ancestor 的一致性域覆盖其子树 +``` + +最后一条很重要:如果祖先字段本身允许整体赋值,那么对子树任意成员的访问都可能与祖先整体替换发生竞争,此时祖先同步域自然必须参与。 + +同步边界来自真实 mutation possibility,而不是单纯来自对象树层级。 + +## 12. Resource_Service 的职责 + +`Resource_Service` 是 Adminive 的应用事务编排层,不是新的 Property Core。 + +一次 managed update 可以包含: + +```text +取得一致性快照 +应用 frontend patch +校验候选对象 +prepare external state +提交 runtime model +commit external side effect +失败时恢复 +生成最终 response snapshot +``` + +当绑定 `Managed_Value` / `Managed_Field` 时,对象一致性由 Structive synchronization 保证。 + +`Resource_Service` 不应该再创建一棵重复的 per-field mutex 系统。 + +## 13. 三种“事务”不能混为一谈 + +Adminive 中至少存在三类不同一致性问题: + +### 13.1 内存对象同步 + +由 Structive synchronization 负责。 + +```text +shared/unique lock +lock slot +synchronization topology +``` + +### 13.2 Adminive 外部副作用事务 + +由 `Resource_Transaction` 负责。 + +```text +prepare +commit +rollback +``` + +它可以代表配置文件、设备、远端服务等外部动作。 + +### 13.3 数据库事务 + +由数据库自身负责。 + +数据库 transaction 不能替代进程内对象锁;对象 mutex 也不能替代数据库 ACID。 + +三者可以在一次业务流程中协作,但职责必须保持独立。 + +## 14. Adapter First + +Adminive Core 不应直接绑定具体第三方库。 + +当前设计通过 Adapter 连接外部生态: + +```text +Json_Adapter +Value_Adapter +Enum_Adapter +Reflection_Adapter +Object_Adapter +Control_Adapter +Polymorphic_Adapter +HTTP adapter +``` + +这条原则有几个直接收益: + +1. 核心协议不因为换 JSON 库而变化。 +2. 枚举反射和结构体反射可以独立替换。 +3. Runtime object 不必为了框架继承某个基类。 +4. 第三方依赖停留在桥接层,不污染业务类型。 +5. 可以针对单个类型提供精确适配,而不是扩大 Core 特例。 + +如果未来接入新的 JSON/HTTP/Reflection 实现,应优先增加 Adapter,而不是修改 Adminive 的中心算法。 + +## 15. Object_Adapter:运行时对象与配置模型分离 + +不是所有运行时对象都适合作为可复制配置对象。 + +例如对象可能包含: + +```text +atomic +mutex +socket +thread +runtime cache +file handle +``` + +`Object_Adapter` 的意义是建立: + +```text +Runtime Object + ⇅ +Pure Model +``` + +Adminive 通过 model 完成: + +```text +JSON +validation +patch +transaction candidate +frontend schema +``` + +然后由 `commit()` 将结果应用回真正的 runtime object。 + +这比要求所有业务类型变成“为了后台框架而设计的 DTO”更符合低侵入原则。 + +## 16. Frontend 应保持被动 + +Adminive 是 backend-driven 系统,因此前端不应该重新维护: + +```text +字段名称 +业务枚举 +显示条件 +校验规则 +CRUD schema +配置树结构 +``` + +前端的职责是: + +```text +获取后端描述 +渲染页面 +提交用户输入 +展示后端错误 +``` + +如果一个业务字段修改后必须同时修改 C++ descriptor 和 TypeScript hard-coded schema,说明 backend-driven 边界已经被破坏。 + +前端可以拥有纯 UI 行为,但不应该成为第二份业务 Schema。 + +## 17. 编译期与运行期的分工 + +Adminive 建议遵守以下规则: + +```text +编译期已知 + → template / concept / requires / descriptor type + +运行期才知道 + → JSON key / HTTP input / runtime resource / dynamic value +``` + +例如 managed writability 是 descriptor type 的静态事实,不应该等一次 HTTP 请求进入以后才通过字符串判断它到底是不是 writable。 + +而 JSON 字段是否存在、用户输入是否合法,则天然只能在运行时处理。 + +这可以同时获得: + +```text +更早的错误 +更清晰的 API +更少的运行时分支 +更好的优化机会 +``` + +## 18. Metadata 必须具有真实语义 + +对 Adminive 来说,metadata 不应该只是“最终转成 JSON 的标签集合”。 + +高质量 metadata 应至少满足一个条件: + +> 它能被某个明确的系统消费,并改变真实行为。 + +例如: + +```text +read_only + → 改变 Structive capability 和同步成本 + +editable + → 改变 update schema 和 managed mutability + +sensitive + → 改变序列化/默认值暴露行为 + +required + → 改变输入验证 + +widget + → 改变前端控件 +``` + +如果新增 metadata 没有明确消费者,应谨慎加入 Core descriptor,避免逐渐变成无边界的“万能标签袋”。 + +## 19. Core 的非目标 + +Adminive Core 不应该演化成: + +```text +通用 ORM +通用 RPC framework +权限系统 +数据库 abstraction +GUI framework +全功能 serialization library +Structive 的替代品 +全局 transaction manager +``` + +这些领域可以通过 Adapter 或上层模块与 Adminive 协作,但不应因为某个项目需要就全部进入核心协议。 + +保持非目标清晰,是长期保持 API 简洁的重要手段。 + +## 20. 新功能应该放在哪里 + +新增能力时,可以按下面的判断顺序决定归属。 + +### 20.1 它描述 Property 本身吗? + +例如: + +```text +intrinsic readable/writable +synchronization topology +generic constraint +``` + +优先考虑 Structive。 + +### 20.2 它描述后台管理领域吗? + +例如: + +```text +editable +widget +list column +AMIS schema +frontend visible condition +``` + +属于 Adminive。 + +### 20.3 它绑定具体第三方库吗? + +例如: + +```text +nlohmann::json +Drogon +cpp-httplib +magic_enum +Boost.PFR +``` + +放 Adapter/bridge 层。 + +### 20.4 它属于具体业务项目吗? + +例如: + +```text +某设备必须先断电才能修改配置 +某用户角色才能执行操作 +某数据库表的迁移规则 +``` + +留在业务层,不进入 Adminive Core。 + +## 21. API 演进原则 + +后续修改 Adminive API 时建议遵守: + +1. 不破坏“Structive 管结构,Adminive 管后台领域”的依赖方向。 +2. 不重新引入重复同步系统。 +3. 不把 frontend permission 当成 Property intrinsic capability。 +4. 静态事实使用类型系统表达,不退化成运行时 bool 判断。 +5. Runtime adapter 保持动态,但不要把动态复杂度传播到普通 typed API。 +6. 一个概念只保留一套主要表达方式,避免 `_key`、mode、view 等重复表面接口无边界增长。 +7. 不为了少几个函数制造难以理解的模板 DSL。 +8. metadata 必须有清晰消费者。 +9. 高级能力允许高级 API,但应与普通使用路径分层。 +10. 对行为语义的修改必须同时补契约测试和文档。 + +## 22. 测试原则 + +Adminive 的测试不应只验证“JSON 长得对”。 + +至少应该覆盖四类契约: + +### 22.1 Descriptor contract + +```text +字段 metadata +managed writable +synchronized +nested descriptor +schema validation +``` + +### 22.2 Managed synchronization contract + +```text +read-only zero-lock +writable locking +unsynchronized path +nested lock propagation +callback lifetime +multi-thread blocking relationship +``` + +### 22.3 Adapter contract + +```text +JSON encode/decode +Enum Adapter +Reflection Adapter +Object Adapter +HTTP Adapter +``` + +### 22.4 Transaction contract + +```text +prepare failure +runtime commit failure +external commit failure +rollback +snapshot consistency +Config Store persistence +``` + +这些测试保护的是设计边界,而不仅是当前实现细节。 + +## 23. 设计判断清单 + +在提交一个新设计前,可以快速检查: + +```text +[ ] 这是 Structive 已经解决的问题吗? +[ ] 这是 Adminive 后台领域真正需要的概念吗? +[ ] 它是否把第三方库耦合进 Core? +[ ] 它是否让 metadata 真正改变行为? +[ ] 它是否把访问控制错误塞进 Property Core? +[ ] 它是否创建了第二套同步或事务机制? +[ ] 静态事实是否能在编译期表达? +[ ] 普通用户是否必须理解这个高级机制? +[ ] 是否存在更合适的 Adapter 层? +[ ] 是否补充了对应契约测试和文档? +``` + +如果多数问题无法明确回答,通常意味着抽象边界还没有收敛。 + +## 24. 最终架构图 + +```text + C++ Business Types + │ + ▼ + Adminive Descriptor + │ + ┌─────────────────┴─────────────────┐ + │ │ + ▼ ▼ + Adminive domain metadata Structive schema + editable/widget/sensitive/... capability/synchronization + │ │ + └─────────────────┬─────────────────┘ + ▼ + Managed object view + │ + ┌─────────────────────┼─────────────────────┐ + ▼ ▼ ▼ + JSON Adapter Resource Service AMIS Schema + │ │ │ + ▼ ▼ ▼ + serialization HTTP transaction Frontend + │ + ▼ + Resource_Transaction + prepare/commit/rollback +``` + +最重要的依赖方向始终是: + +```text +Structive + ↑ +Adminive Core + ↑ +Adapters / Service + ↑ +Application +``` + +底层不知道上层领域,上层消费底层能力。这是 Adminive 后续演进时最需要守住的架构边界。