Files
Adminive/README.md
T
2026-08-07 09:25:55 +08:00

642 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Adminive Backend-Driven Complete Example
Adminive 使用 C++20 模板、Concepts 和可替换适配器描述业务类型。同一份后端描述驱动 JSON 双向转换、事务式赋值、字段权限、校验、amis CRUD、枚举列表列、树状配置、联动条件、日期控件、颜色控件和一次性提交。核心层不依赖具体 JSON 库、枚举反射库或结构体反射库。前端只读取 `/admin/amis` 并渲染后端返回的页面 JSON,不包含业务字段或业务判断。
## 后端分层
`backend/library` 是库级协议层,只有头文件,不依赖也不链接任何具体 JSON、HTTP Server、枚举反射或结构体反射实现。它包含 descriptor、JSON 适配协议、AMIS schema、状态协议、同步协议和抽象 `Resource_Service`
```cmake
target_link_libraries(your_target PRIVATE Adminive::Core)
```
`Adminive::Http``Adminive::Core` 指向同一套纯头文件协议层,只用于表达消费侧语义,不额外引入依赖。
`backend/service` 是服务桥接层,放置具体适配器、第三方头文件和示例服务。需要什么桥接就显式选择什么目标:
```cmake
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/adapters/nlohmann_json.hpp``magic_enum.hpp``boost_pfr.hpp``httplib.hpp``drogon.hpp` 都属于服务桥接层。核心协议不会自动包含它们。项目默认示例使用:
```cpp
#include "adminive/adapters/nlohmann_json.hpp"
#include "adminive/adapters/magic_enum.hpp"
using Json = nlohmann::json;
```
### JSON 适配
外部 JSON 类型通过 `Json_Adapter<Json>` 接入。适配器负责对象、数组、标量、字段访问、解析和输出。所有转换入口显式携带 JSON 类型:
```cpp
auto descriptor = adminive::to_descriptor_json<Json, Config>();
auto encoded = adminive::to_json<Json>(config);
auto result = adminive::apply_frontend_patch<Json>(config, input);
```
### 外部值类型适配
外部包装类型不需要继承 Adminive 类型。可以针对全部 JSON 实现提供通用适配,也可以只针对某个 JSON 类型特化:
```cpp
template <class Json>
struct adminive::Value_Adapter<Copyable_Atomic_Int, Json> {
using value_type = int;
static int read(const Copyable_Atomic_Int& value) noexcept {
return value.load();
}
static void write(Copyable_Atomic_Int& target, int value) noexcept {
target.store(value);
}
};
```
需要完全控制某种 JSON 的编码时,适配器可以提供 `encode()``decode()``type_name``append_schema()`
### 枚举适配
核心仅调用 `Enum_Adapter<Enum>``adminive/adapters/magic_enum.hpp` 是可选桥接,也可以为单个枚举自行实现:
```cpp
template <>
struct adminive::Enum_Adapter<My_Mode> {
static std::vector<My_Mode> values();
static std::string_view name(My_Mode value);
static std::optional<My_Mode> cast(std::string_view value);
};
```
### PFR 或其他结构体反射适配
核心通过 `Reflection_Adapter<T>` 接收字段数量、字段名称和按索引访问。可选桥接头为:
```cpp
#include "adminive/adapters/boost_pfr.hpp"
```
普通聚合类型:
```cpp
template <>
struct adminive::Reflection_Adapter<Config>
: adminive::Boost_Pfr_Reflection_Adapter<Config> {};
```
由多个数据基类组成的配置类型可以展开所有基类字段:
```cpp
template <>
struct adminive::Reflection_Adapter<Mode_ACS_Config>
: adminive::Boost_Pfr_Base_Reflection_Adapter<
Mode_ACS_Config,
Mode_ACS_Config_Base_Data,
Mode_ACS_Config_Data> {};
```
较旧 Boost.PFR 不提供字段名时,外部为每个数据基类特化 `Boost_Pfr_Name_Adapter<T>`;较新版本自动使用 `boost::pfr::get_name()`
### 不可复制运行时对象
`Object_Adapter<T>` 将不可复制、含原子字段或带运行时状态的对象映射到纯值配置模型。读取只要求 `snapshot()`,编辑要求 `snapshot()``commit()`,创建能力才额外要求 `create()`
```cpp
template <>
struct adminive::Object_Adapter<Runtime_Config> {
using model_type = Runtime_Config_Model;
static model_type snapshot(const Runtime_Config& value);
static void commit(Runtime_Config& target, model_type value);
};
```
`Resource_Service` 始终通过 `Object_Adapter<T>::commit()`提交运行时模型。外部持久化和系统副作用通过 `Resource_Transaction<Model>` 分成准备、提交和回滚三个阶段:
```cpp
adminive::Resource_Transaction<Runtime_Config_Model> transaction;
transaction.prepare = [](const Runtime_Config_Model& candidate, const adminive::Request_Context& context) {
validate_external_state(candidate, context);
};
transaction.commit = [](const Runtime_Config_Model& candidate, const adminive::Request_Context&) {
persist(candidate);
};
transaction.rollback = [](const Runtime_Config_Model& original, const adminive::Request_Context&) {
persist(original);
};
adminive::Resource_Service<Runtime_Config, Json> resource(runtime, "/config", std::move(transaction));
```
`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 的默认策略是:`.editable()` 字段自动拥有字段锁;普通只读字段不拥有字段锁;`.locked()` 可以强制给不可编辑但会被程序动态修改的字段启用字段锁;`.locked(false)` 可以显式关闭某个可编辑字段的字段锁。
```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>();
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;
});
```
描述协议 `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 生成时给出明确错误。
```cpp
template <>
struct adminive::Control_Adapter<Color, Json> {
static Json make_control(const Json& field, adminive::Control_Context context);
static Json make_column(const Json& field);
};
```
### 多态对象
`Polymorphic_Adapter<T, Json>` 返回带真实 C++ 类型的 variant 元组。Adminive 因此能继续调用每个派生类型字段的 `Control_Adapter`,不会退回无类型的 JSON 控件路径。
```cpp
template <>
struct adminive::Polymorphic_Adapter<Device, Json> {
static constexpr std::string_view discriminator() noexcept {
return "type";
}
static constexpr std::string_view discriminator_label() noexcept {
return "设备类型";
}
static auto variants() {
return std::tuple(
adminive::polymorphic_variant<Serial_Device>("serial", "串口设备"),
adminive::polymorphic_variant<Network_Device>("network", "网络设备")
);
}
static Json encode(const Device& value);
static void decode(Device& target, const Json& value, adminive::Write_Context context);
};
```
`decode_polymorphic_alternative<Json, Variant>()` 在判别类型不变时执行补丁更新并保留未提交字段;切换类型时通过该类型的 `Object_Adapter::create()` 创建候选对象,只读取新类型声明的字段。
### 嵌套错误和 HTTP 反馈
字段错误使用完整路径,例如 `map_resources.tile_sources[2].resource.maximum_level`。字段赋值、对象校验和事务提交抛出的 `Field_Validation_Error` 都会转换为 HTTP 422,并同时返回 `errors``field_errors`
### 描述器校验和敏感字段
描述器生成时会拒绝空字段名、重复字段名以及不存在或不可排序的默认排序字段。普通描述器默认不输出字段默认值;需要默认值时显式调用:
```cpp
auto descriptor = adminive::to_descriptor_json_with_defaults<Json, Config>();
```
敏感字段使用:
```cpp
ADMINIVE_FIELD(T, password).sensitive().editable();
```
敏感字段不会进入前端数据,也不会进入带默认值的描述器。
### Drogon 生命周期和异步提交
`Drogon_Resource` 的路由回调捕获共享的 `Resource_Service`,绑定完成后销毁 binder 不会留下悬空回调。`Drogon_Bind_Options` 同时配置异步执行器、请求上下文和 Drogon Filter
```cpp
adminive::Drogon_Bind_Options options;
options.executor = [](std::function<void()> task) {
worker_pool.submit(std::move(task));
};
options.context_factory = [](const drogon::HttpRequestPtr& request) {
adminive::Request_Context context;
context.user = current_user(request);
context.remote_address = request->peerAddr().toIp();
return context;
};
options.filters = {"LoginFilter", "AdminPermissionFilter"};
resource.bind(drogon::app(), std::move(options));
```
整体状态使用 `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。
### 描述器驱动状态
整体状态和列表项状态都使用 `adminive.status` 描述器生成,不再包含固定的服务名、字段名或模板。服务标题、字段标签、颜色和轮询周期由调用端传入的状态描述器决定。
## 目录结构
```text
Adminive/
├── backend/
│ ├── library/
│ │ ├── include/adminive/
│ │ ├── tests/
│ │ └── CMakeLists.txt
│ ├── service/
│ │ ├── include/adminive/adapters/
│ │ ├── src/
│ │ ├── tests/
│ │ ├── third_party/
│ │ └── CMakeLists.txt
│ └── CMakeLists.txt
├── cmake/
├── frontend/
├── scripts/
├── CMakeLists.txt
└── README.md
```
`example.hpp` 位于 `backend/service/src`,只作为完整示例实现,不属于公共库头文件。
## 列字段名称、显示名称和排序
```cpp
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
.creatable()
.editable()
.required()
.list_label("Mode")
.order(5)
.sortable();
```
- `name` 来自成员名称,用于 JSON 字段和接口参数。
- `label` 用于表单标签。
- `list_label` 用于列表表头,未设置时回退到 `label`
- `order` 控制列表列顺序,未设置时按字段声明顺序生成默认值。
- `sortable` 控制该列能否排序,后端只接受已声明为可排序的字段。
## 枚举列表项
```cpp
enum class Radio_Mode {
receive,
transmit,
duplex,
maintenance
};
```
业务结构直接使用枚举:
```cpp
struct Radio_State {
Radio_Mode mode{Radio_Mode::receive};
};
```
描述中不需要手写选项:
```cpp
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
.creatable()
.editable()
.required()
.list_label("Mode")
.order(5)
.sortable();
```
示例通过可选的 `magic_enum` 桥接生成;核心只依赖 `Enum_Adapter`
```json
{
"name": "mode",
"value_type": "enum",
"options": [
{"label": "Receive", "value": "receive"},
{"label": "Transmit", "value": "transmit"},
{"label": "Duplex", "value": "duplex"},
{"label": "Maintenance", "value": "maintenance"}
]
}
```
表格列使用 amis `mapping`,接口数据仍然返回稳定的枚举字符串,例如 `"duplex"`
## 树状配置
树结构由嵌套的已描述对象表达,不额外维护一份前端树配置:
```cpp
struct Endpoint_Config {
bool enabled{true};
std::string host{"127.0.0.1"};
Range_Value<int, 1, 65535> port{9000};
};
struct Appearance_Config {
std::string panel_title{"Radio Control"};
std::string effective_date{"2026-08-06"};
std::string accent_color{"#2563eb"};
};
struct Radio_Service_Config {
std::string profile_name{"Primary Radio Profile"};
Radio_Mode mode{Radio_Mode::receive};
Range_Value<int, 1, 64> worker_count{4};
double receive_gain{1.25};
Endpoint_Config primary_endpoint{};
Endpoint_Config backup_endpoint{};
Appearance_Config appearance{};
};
```
界面结构为:
```text
Radio Service Configuration
├── Profile Name
├── Operating Mode
├── Worker Count
├── Receive Gain
├── Primary Endpoint
│ ├── Enable Endpoint
│ ├── Host Address
│ └── Port
├── Backup Endpoint
│ ├── Enable Endpoint
│ ├── Host Address
│ └── Port
└── Appearance
├── Panel Title
├── Effective Date
└── Accent Color
```
嵌套对象在 amis 表单中渲染成可折叠 `fieldset`,子字段使用点路径,例如:
```text
primary_endpoint.host
appearance.effective_date
```
提交时仍然形成嵌套 JSON 对象。
## 配置联动
字段描述通过 `visible_on` 保存 amis 表达式:
```cpp
ADMINIVE_FIELD_LABEL(T, backup_endpoint, "Backup Endpoint")
.editable()
.visible_on("${$self.mode == 'duplex'}");
```
`mode` 不是 `duplex` 时,整个 `Backup Endpoint` 子树隐藏。
第二个配置示例使用组合条件:
```cpp
ADMINIVE_FIELD_LABEL(T, webhook_endpoint, "Webhook Endpoint")
.editable()
.visible_on("${$self.enabled && $self.channel == 'webhook'}");
```
只有启用告警且投递通道为 `webhook` 时,才显示 webhook 子配置。
`$self` 表示当前对象的数据域。顶层字段会解析为 `${mode ...}`,嵌套对象会自动补成 `${parent.mode ...}`,避免复用子配置描述器时引用到错误的数据域。
## 完整输入控件
后端根据 C++ 类型自动选择基础控件:
```text
std::string -> input-text
整数类型 -> input-number
浮点类型 -> input-number
bool -> switch
enum class -> select
```
`std::optional<bool>``std::optional<int>``std::optional<double>``std::optional<std::string>` 和可适配枚举使用相同基础控件并自动启用清空;JSON `null` 对应 `std::nullopt``std::string_view` 只允许作为只读存储。
日期和颜色通过后端字段描述指定:
```cpp
ADMINIVE_FIELD_LABEL(T, effective_date, "Effective Date")
.editable()
.required()
.widget("input-date");
ADMINIVE_FIELD_LABEL(T, accent_color, "Accent Color")
.editable()
.required()
.widget("input-color");
```
日期控件自动补充:
```json
{
"type": "input-date",
"valueFormat": "YYYY-MM-DD",
"displayFormat": "YYYY-MM-DD",
"clearable": true
}
```
## 一次性确认修改
配置表单不会在单个输入项改变时调用后端。所有修改先保留在表单数据域,点击 `Confirm Changes` 后一次性提交完整配置:
```json
{
"actions": [
{"type": "reset", "label": "Reset"},
{"type": "submit", "label": "Confirm Changes", "level": "primary"}
]
}
```
后端先复制当前对象,在副本上完成所有字段赋值和校验,全部成功后才替换原对象,因此一次提交具有对象级事务语义。
## 两组配置示例
页面包含四个后端定义的标签页:
```text
Radio State List
Radio Service Configuration
Alert Configuration
Device Tables
```
`Radio Service Configuration` 展示字符串、枚举、整数、浮点数、树状子配置、日期和颜色。
`Alert Configuration` 展示布尔开关、字符串、两个枚举、数字、日期、颜色和条件显示的 webhook 子配置。
## 对象状态
对象状态使用独立返回类型描述,不混入 CRUD 对象字段:
```cpp
struct Radio_Item_Status {
Status_Value<Radio_Operating_State> operating_state;
Status_Value<std::uint64_t> refresh_sequence;
Status_Value<std::string> refresh_time;
};
```
状态值同时携带颜色:
```json
{
"operating_state": {
"value": "running",
"color": "#16a34a"
}
}
```
资源通过成员函数指针注册:
```cpp
resource.register_status<&Radio_State::status>(2000);
```
只有用户打开某一行的 `View status` PopOver 后,该行状态才开始请求和刷新。
## API
```text
GET /admin/amis
GET /admin/status
GET /admin/radio_states/descriptor
GET /admin/radio_states/status/descriptor
GET /admin/radio_states/amis
GET /admin/radio_states
GET /admin/radio_states/{id}
GET /admin/radio_states/{id}/status
POST /admin/radio_states
PUT /admin/radio_states/{id}
PATCH /admin/radio_states/{id}
DELETE /admin/radio_states/{id}
GET /admin/config/radio/descriptor
GET /admin/config/radio/data
GET /admin/config/radio/amis
POST /admin/config/radio/data
GET /admin/config/alerts/descriptor
GET /admin/config/alerts/data
GET /admin/config/alerts/amis
POST /admin/config/alerts/data
```
## 前端
创建外部 `node_modules` 相对符号链接:
```powershell
pwsh -NoProfile -File .\scripts\link_node_modules.ps1
```
默认结构:
```text
Adminive/
├── frontend/
│ └── node_modules -> ../node_modules
└── node_modules/
```
安装和构建:
```powershell
npm --prefix .\frontend ci
npm --prefix .\frontend run build
```
## 后端
```powershell
cmake -S . -B build -G Ninja
cmake --build build
ctest --test-dir build --output-on-failure
.\build\backend\Adminive_Server.exe 9999
```
安装并通过 CMake 包使用:
```powershell
cmake --install build --prefix D:/Adminive
```
```cmake
find_package(Adminive CONFIG REQUIRED)
target_link_libraries(app PRIVATE Adminive::Core)
find_package(Adminive CONFIG REQUIRED COMPONENTS Drogon)
target_link_libraries(drogon_app PRIVATE Adminive::Drogon)
```
MSVC 消费者默认接收 `/utf-8``/Zc:__cplusplus``/permissive-` 只用于 Adminive 自身目标;确实需要传播时配置 `ADMINIVE_PROPAGATE_MSVC_STRICT_MODE=ON`
访问:
```text
http://127.0.0.1:9999
```
## 源码压缩目标
```powershell
cmake --build build --target package_zip
```
压缩函数依次接收目标名、输出文件、根目录、包含列表变量名和排除列表变量名。只扫描包含列表指定的路径,再按相对于根目录的规则排除:
```cmake
add_project_zip_target(
package_zip
"${CMAKE_CURRENT_LIST_DIR}/Adminive.zip"
"${CMAKE_CURRENT_LIST_DIR}"
adminive_package_includes
adminive_package_excludes
)
```