拆分核心库和服务

This commit is contained in:
2026-08-07 09:25:55 +08:00
parent 6fb1bbed61
commit bba2895e2b
101 changed files with 1915 additions and 985 deletions
+63 -31
View File
@@ -2,25 +2,26 @@
Adminive 使用 C++20 模板、Concepts 和可替换适配器描述业务类型。同一份后端描述驱动 JSON 双向转换、事务式赋值、字段权限、校验、amis CRUD、枚举列表列、树状配置、联动条件、日期控件、颜色控件和一次性提交。核心层不依赖具体 JSON 库、枚举反射库或结构体反射库。前端只读取 `/admin/amis` 并渲染后端返回的页面 JSON,不包含业务字段或业务判断。
## 适配
## 后端分
核心目标只提供描述器、转换流程和 amis Schema 生成
`backend/library` 是库级协议层,只有头文件,不依赖也不链接任何具体 JSON、HTTP Server、枚举反射或结构体反射实现。它包含 descriptor、JSON 适配协议、AMIS schema、状态协议、同步协议和抽象 `Resource_Service`
```cmake
target_link_libraries(your_target PRIVATE Adminive::Core)
```
HTTP 资源服务与具体服务器分层:
`Adminive::Http``Adminive::Core` 指向同一套纯头文件协议层,只用于表达消费侧语义,不额外引入依赖。
`backend/service` 是服务桥接层,放置具体适配器、第三方头文件和示例服务。需要什么桥接就显式选择什么目标:
```cmake
target_link_libraries(your_target PRIVATE Adminive::Http)
target_link_libraries(json_target PRIVATE Adminive::Nlohmann)
target_link_libraries(enum_target PRIVATE Adminive::MagicEnum)
target_link_libraries(httplib_target PRIVATE Adminive::Httplib)
target_link_libraries(drogon_target PRIVATE Adminive::Drogon)
```
`Adminive::Http` 只提供 `Resource_Service`、HTTP 响应模型和错误结构,不包含服务器头文件。`adminive/adapters/httplib.hpp` `adminive/adapters/drogon.hpp` 分别负责路由注册和响应写回。
项目自带的 `nlohmann::json``magic_enum` 桥接不会被核心头自动包含。需要默认实现时显式包含:
`adminive/adapters/nlohmann_json.hpp``magic_enum.hpp``boost_pfr.hpp``httplib.hpp` `drogon.hpp` 都属于服务桥接层。核心协议不会自动包含它们。项目默认示例使用:
```cpp
#include "adminive/adapters/nlohmann_json.hpp"
@@ -129,31 +130,65 @@ transaction.rollback = [](const Runtime_Config_Model& original, const adminive::
adminive::Resource_Service<Runtime_Config, Json> resource(runtime, "/config", std::move(transaction));
```
`Resource_Service` 对一次更新持有同一把对象锁,锁覆盖快照、补丁应用、`prepare`、运行时 `commit`、外部 `commit`、失败恢复和响应快照。这样外部业务线程只要通过同一个同步对象访问配置,就不会读到跨字段撕裂状态,也不会在持久化提交期间覆盖候选配置。事务回调应保持短小;使用非递归锁时,回调不能再次进入同一个同步对象,而应调用已经处于锁内的持久化实现。
`Resource_Service` 对一次更新持有同一个一致性作用域,锁覆盖快照、补丁应用、`prepare`、运行时 `commit`、外部 `commit`、失败恢复和响应快照。绑定根 `Synchronized_Value` 时独占根 barrier;绑定 `Synchronized_Field` 子对象时默认只独占对应子对象节点并持有祖先共享 barrier,不会为了修改一个子配置锁死整个根配置。如果事务回调需要读取、复制或序列化整个根配置,则必须把资源作用域设为 `Resource_Lock_Scope::root`,让整个事务持有根独占 barrier。事务回调应保持短小;使用非递归锁时,回调不能再次进入同一个同步作用域,而应调用已经处于锁内的持久化实现。
```cpp
auto radio = config.member<&Application_Config::radio_service>();
adminive::Resource_Service<Radio_Service_Config, Json> resource(
radio,
"/config/radio",
transaction,
adminive::Resource_Lock_Scope::root
);
```
`local` 是子资源默认值,适用于事务只操作该子对象或对应数据库行的场景;`root` 只用于事务确实需要根对象一致性快照的场景,例如把整个 `Application_Config` 写入一个 JSON 配置文件。
### 配置同步协议
配置是否写入文件或数据库,与进程内是否需要加锁是两件事。数据库负责持久化和跨进程事务;同一进程里的 HTTP 线程、工作线程和业务线程仍会同时访问运行时配置,因此默认应对整个配置结构体加锁。字段级锁无法保证多个字段之间的约束和序列化快照一致性,Adminive 不提供字段级隐式锁。
持久化和进程内同步是两个独立问题。配置写入文件或数据库后,HTTP 线程、业务线程和后台任务仍可能同时访问运行时对象,因此动态字段仍需要进程内同步。Adminive 的默认策略是:`.editable()` 字段自动拥有字段锁;普通只读字段不拥有字段锁;`.locked()` 可以强制给不可编辑但会被程序动态修改的字段启用字段锁;`.locked(false)` 可以显式关闭某个可编辑字段的字段锁。
`Synchronized_Value<T, Lock>` 持有配置和值锁,`read()``write()``snapshot()``member<Member>()` 是统一访问协议。成员视图与根对象共享同一把锁,因此编辑子配置时仍会锁住整个根配置:
```cpp
return adminive::object<Config>(
"config",
ADMINIVE_FIELD(Config, editable_value).editable(),
ADMINIVE_FIELD(Config, startup_only_value),
ADMINIVE_FIELD(Config, backend_mutable_value).locked(),
ADMINIVE_FIELD(Config, no_dedicated_field_lock).editable().locked(false)
);
```
`Synchronized_Value<T, Object_Lock, Field_Lock>` 内部同时维护根对象 barrier 和层级字段锁。所有字段读取至少持有根共享 barrier,保证它不会和整对象提交、替换或序列化事务并发;普通只读字段不再额外获取字段锁。可编辑或显式 `.locked()` 的字段再获取对应的层级字段锁,因此不同动态字段仍可以并发。`.locked(false)` 只关闭当前字段自己的专用字段锁;如果该字段是结构体且后代存在动态字段,后代所需的结构 barrier 仍然保留。对未启用字段锁的字段执行写操作时会直接升级为根独占操作,避免出现无保护写入。序列化、反序列化、对象级校验和整对象事务属于一致性操作,会独占被操作对象的 barrier,避免跨字段撕裂快照。嵌套对象序列化只独占该子对象节点,同时持有祖先共享锁,不会无条件锁死整个根配置。
```cpp
adminive::Synchronized_Value<Application_Config> config;
auto radio = config.member<&Application_Config::radio_service>();
adminive::Resource_Service<Radio_Service_Config, Json> resource(radio, "/config/radio", transaction);
auto snapshot = config.snapshot();
config.write([](Application_Config& value) {
value.default_device_type = Device_Table_Type::network;
auto worker_count = radio.member<&Radio_Service_Config::worker_count>();
worker_count.write([](auto& value) {
value = 8;
});
auto snapshot = radio.snapshot();
auto root_snapshot = config.snapshot();
adminive::Synchronized_Value<Pfr_Config> reflected_config;
reflected_config.field<0>().write([](auto& value) {
value = 8;
});
```
直接调用 `to_json()``to_frontend_json()``assign_json()``apply_frontend_patch()` 处理同步对象和成员视图时也会进入同一把锁。确实由外部生命周期保证单线程访问时,将锁模板参数替换为 `Empty_Lock`,接口和执行路径不变:
描述协议 `adminive.resource` 版本为 3。每个字段会输出 `lock_mode` 和最终计算后的 `synchronized`,因此服务层能够明确知道该字段的并发语义,而不是由具体 JSON 或 HTTP 后端隐式决定。
数据库事务不能替代这套运行时锁。`Resource_Service` 在一次 HTTP 更新中持有对应资源的一致性锁作用域,覆盖候选快照、补丁应用、对象校验、运行时提交、持久化回调、失败回滚和响应快照。数据库负责持久化原子性;对象/字段 barrier 负责当前进程中的运行时一致性。
如果对象生命周期已经由外部严格保证为单线程,可以统一换成 `Empty_Lock`,不需要维护另一套无锁实现:
```cpp
adminive::Synchronized_Value<Application_Config, adminive::Empty_Lock> config;
adminive::Resource_Service<Application_Config, Json, adminive::Empty_Lock> resource(config, "/config");
```
同步对象不公开原始值和 mutex。普通业务代码只能通过 `read()``write()``snapshot()``replace()`、显式成员的 `member<&T::x>()` 和反射字段的 `field<Index>()` 进入同步协议,避免重新出现 `data() + mutex()` 由调用方手工配对的问题。同步回调会静态拒绝引用、指针、`reference_wrapper` 和 ranges view 等常见引用逃逸形式;自定义返回类型如果内部保存受保护对象的地址或引用,仍由调用方保证不会把它带出锁作用域。
### 外部控件适配
复杂业务类型通过 `Control_Adapter<T, Json>` 明确决定表单控件和列表列。框架不会把 `vector``map` 隐式决定为表格、列表、分页或标签页;未配置控件的结构类型会在 Schema 生成时给出明确错误。
@@ -231,7 +266,7 @@ options.filters = {"LoginFilter", "AdminPermissionFilter"};
resource.bind(drogon::app(), std::move(options));
```
整体状态使用 `Drogon_Status_Resource<Status, Json>` 注册 `/descriptor``/amis``/data`。所有 Drogon 路由都会把异常转换成结构化 HTTP 500。
整体状态使用 `Drogon_Status_Resource<Status, Json>` 注册 `/descriptor``/amis``/data`列表 CRUD 使用 `Drogon_Collection_Resource<T, Json>`,和 httplib 的 `Http_Collection_Resource<T, Json>` 共用纯头文件 `Collection_Service<T, Json>`Drogon/httplib 只负责路由参数、请求体和响应桥接,不再各自实现 CRUD 协议。当前 `Collection_Service` 是线程安全的内存集合服务,尚未定义数据库 repository/transaction 协议;需要把列表直接落数据库时,应在这一层补持久化抽象,而不是把数据库逻辑塞回 Drogon 或 httplib binder。所有 Drogon 路由都会把异常转换成结构化 HTTP 500。
### 描述器驱动状态
@@ -242,20 +277,17 @@ resource.bind(drogon::app(), std::move(options));
```text
Adminive/
├── backend/
│ ├── include/adminive/
│ ├── src/
│ │ ├── example.hpp
│ │ ── example.cpp
│ ├── example_descriptors.hpp
│ │ ├── config_store.hpp
│ │ ├── config_store.cpp
│ │ ├── device_tables.hpp
│ │ ├── device_tables.cpp
│ │ ── server_app.hpp
│ ├── server_app.cpp
│ │ └── main.cpp
│ ├── tests/
│ └── third_party/
│ ├── library/
│ ├── include/adminive/
│ │ ├── tests/
│ │ ── CMakeLists.txt
│ ├── service/
│ │ ├── include/adminive/adapters/
│ │ ├── src/
│ │ ├── tests/
│ │ ├── third_party/
│ │ ── CMakeLists.txt
└── CMakeLists.txt
├── cmake/
├── frontend/
├── scripts/
@@ -263,7 +295,7 @@ Adminive/
└── README.md
```
`example.hpp` 位于 `backend/src`,只作为完整示例实现,不属于公共库头文件。
`example.hpp` 位于 `backend/service/src`,只作为完整示例实现,不属于公共库头文件。
## 列字段名称、显示名称和排序