Files
Adminive/README.md
T
2026-08-12 01:44:13 +08:00

936 lines
44 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 和可替换适配器,把后端已有的 C++ 业务结构投影成后台管理界面。同一份后端定义同时驱动对象结构 JSON、数据访问、字段控件、表单/卡片/流式/网格/Tabs/表格等语义组合、查询能力、HTTP 接口和 AMIS 页面。属性能力、Schema 与同步拓扑由 `third_party/Structive` 提供;Adminive 在其上分层描述 Field Presentation、View Schema、页面 Composition、JSON/HTTP/AMIS 和事务适配。前端不维护第二份业务字段清单,只消费后端生成的 `/descriptor``/view``/data``/amis`
## 设计理念
Adminive 的核心定位是:**让后端已有的 C++ 结构直接驱动后台管理能力,而不是再建立一套独立的数据模型。**
整体职责按语义分层:
```text
Structive / Object Schema
Property、类型、intrinsic read/write、constraint、synchronization
Adminive Field Presentation
单个字段的 label、description、control、options、visible_on
Adminive View Schema
Form/Table 的字段选择、顺序、分组和数据交互能力
Adminive Composition View
Form/Table/Status 等组件之间的 card/grid/flow/tabs 等页面组合
AMIS Adapter / Frontend
翻译成具体 AMIS JSON,并负责最终视觉样式和响应式表现
```
当前设计遵守几个核心原则:
- Structive 管“对象本身是什么”,Adminive 管“后台系统怎样消费它”。
- 字段默认 intrinsic read-only,只有 `.editable()``.creatable()``.read_write()` 显式进入 managed mutation path。
- `read_only` 是能够影响真实运行成本的结构信息:只读存储属性不为自身贡献 lock slot/mutexmanaged read 走零锁路径。
- `editable``sensitive``Field_Presentation` 是 Adminive 领域 metadata;表格排序、搜索、筛选和页面组合属于独立 `View Schema`,都不是通用 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)。
### 后端 UI 的三层描述与组合层
一个字段“是什么”、一个字段“怎么呈现”、一组字段“怎么组成业务视图”,以及多个业务视图“怎么组成页面”是四个不同问题。Adminive 不再把它们塞进同一个 Field Descriptor
```text
Object Schema 结构事实
Field Presentation 单字段呈现
View Schema Form/Collection 语义组合
Composition Manifest 多组件页面组合与 Slot Contract
```
后端保留完整的组合能力,包括 `frontend``vertical``horizontal``flow``grid``list``group``card``tabs`,集合展示则由一等 `Collection_View` 表达 `table/list/cards` mode。`frontend` 是显式的布局授权节点:后端只声明成员与顺序,最终横排、竖排、列表、卡片、表格或响应式位置由前端决定;其余组合节点仍由后端决定语义位置。字段定义不携带集合列顺序/排序等信息,AMIS Adapter 也不承担业务组合决策。
例如一个配置对象可以只定义字段事实和单字段控件:
```cpp
ADMINIVE_FIELD(T, host).editable().label("Host").text_input();
ADMINIVE_FIELD(T, port).editable().label("Port").number_input();
ADMINIVE_FIELD(T, color).editable().label("Color").color_input();
```
同一个类型的编辑视图再独立决定组合:
```cpp
return edit_form<T>(vertical(
group("Network", horizontal(use<&T::host>(), use<&T::port>())),
card("Appearance", use<&T::color>())
));
```
如果同一个类型还需要集合展示,先定义统一 Collection View,再只切换展示 mode
```cpp
const auto base = collection_view<T>(
column<&T::host>("Host").search().fix(Table_Fixed::left),
column<&T::port>("Port").sort(),
column<&T::color>("Color")
).default_sort("port");
const auto item = collection_item<&T::host>().body<&T::port, &T::color>();
const auto table = base.as_table();
const auto list = base.as_list(item);
const auto cards = base.as_cards(item, 3);
```
其中 `.sort()``.search()``.filter()` 不只是前端提示:Collection Service 只接受后端 View Schema 明确声明的查询能力。前端不能自行把一个后端不支持的字段变成可排序或可搜索字段。
对于“上面一个配置表单、下面一个设备表格”这类跨组件页面,使用 `Composition_View`
```cpp
const auto page = composition_view("device_page", compose::vertical(
compose::heading("Devices"),
compose::slot("config"),
compose::slot("table")
));
```
`slot` 引用已经生成的 Form/Collection/Status 等组件。普通 Composition 节点由后端决定语义组合,AMIS Adapter 只负责翻译;`compose::frontend(...)` 则明确把最终定位权交给前端,同时仍由后端决定页面有哪些组件。自定义前端通过 `adminive.composition-manifest` 同时取得 Composition View 和正式 Slot ContractContract 明确组件类型以及 descriptor/view/data/amis 来源,避免前端再维护第二份业务注册表。这样两种模式可以在同一个项目里并存。
### 示例组件画廊
完整示例首页现在包含“组件画廊”页签。画廊不是前端写死的组件清单,而是后端使用 Adminive 自身 Descriptor、View Schema、Composition View 和 Collection Service 生成,用于直接检查当前支持能力的实际 AMIS 效果。
字段画廊覆盖 `automatic``text``multiline_text``number``boolean``select``date``color``std::optional`、只读 static、sensitive、条件联动、嵌套对象、Range/Validated Value,并用 Field Policy Matrix 单独展示 `readable``read_write``editable``creatable``required``include_default` 和 synchronization 的独立语义;Form View 覆盖 display/create/edit 三种 mode 以及 `frontend``vertical``horizontal``grid``flow``list``group``card``tabs`。Collection 使用同一份后端字段、查询和 CRUD 事实,由一等 `Collection_View` 直接表达 `table/list/cards` 三种 mode,不再生成 table AMIS 后二次改写。Composition 画廊覆盖 `slot`、正式 Slot Contract、`heading``frontend``vertical``horizontal``flow``grid``list``group``card``tabs``tab`。高级能力页提供实际可操作的 Object/Value/Control Adapter、Polymorphic Adapter、Sensitive、Resource Transaction、422 字段错误和 Status Polling Demo。
React 首页现在本身就是文档与组件画廊:可以切换“后端组合定位”和“前端决定定位”。前端定位页递归读取 Composition View 的 slot 顺序,从 Manifest 的 Slot Contract 取得真实 Form/List/Cards 组件,然后把**同一批组件**分别组合成横排、竖排、列表、卡片和表格;React 不写死 slot 名,也不知道 `Gallery_Row` 的业务字段。活文档页直接并排展示实际效果对应的 C++ 声明、`adminive.view`/Manifest JSON、最终 AMIS JSON、HTTP 接口和真实数据。
能力清单由后端 `Capability Case Registry` 统一生成。每个 Case 同时绑定 `id/group/title/summary`、C++ 声明、View/Manifest JSON、运行时或 AMIS JSON、实际 preview、tags 和 endpoints;首页能力矩阵、活文档和接口索引都从这些 Case 派生,不再维护一份“声称支持但没有 Demo”的静态字符串表。当前 Case 覆盖字段权限、三种 Form mode、两种定位权、三种 Collection mode、Json/Enum/Reflection/Value/Control/Object/Polymorphic Adapter、Managed 同步拓扑、Transaction 四条路径、Status、HTTP Adapter、协议边界和工程验证 Gate。
Collection 页面同时是能力实验台。分页、排序、搜索、筛选、row reorder、item status、overview status 和非法 query 都执行真实 HTTPcolumn reorder 是纯前端表现状态,因此实验台明确展示后端 `column_reorderable` 授权和本地列顺序变化,而不会伪造业务提交。协议边界页每次都会重新执行 `grid(0)`、非法 tab、缺失/重复 Slot Contract、重复 slot、readonly 提交、nested 多字段 422、object validator、多态 discriminator 和非法 Collection query。
画廊资源同时提供正常 Adminive HTTP 接口:
```text
/admin/amis
/admin/gallery/config/descriptor
/admin/gallery/config/view
/admin/gallery/config/data
/admin/gallery/config/amis
/admin/gallery/items/descriptor
/admin/gallery/items/view
/admin/gallery/items/amis
/admin/gallery/items
/admin/gallery/items/{id}
/admin/gallery/items/order
/admin/gallery/items/list/view
/admin/gallery/items/cards/view
/admin/gallery/items/table/view
/admin/gallery/items/list/amis
/admin/gallery/items/cards/amis
/admin/gallery/items/table/amis
/admin/gallery/advanced/adapter/{descriptor,view,data,amis}
/admin/gallery/advanced/reflection/{descriptor,view,data,amis}
/admin/gallery/advanced/polymorphic/{descriptor,view,data,amis}
/admin/gallery/advanced/object/{descriptor,view,data,amis}
/admin/gallery/items/{id}/status
/admin/gallery/managed
/admin/gallery/boundaries
/admin/gallery/items?score=92
/admin/gallery/transaction/{success|prepare-reject|commit-fail|rollback-fail}
/admin/gallery/backend/view
/admin/gallery/backend/amis
/admin/gallery/frontend/manifest
/admin/gallery/docs
```
`flow` 在 AMIS Adapter 中翻译成可换行 Flex,每个 `items` 元素都是显式 `container` renderer。`list` 保留条目语义并生成明确的条目容器;`frontend` 在 AMIS 中使用中性竖向 fallback,但原始 View JSON 保留 `kind: "frontend"`,自定义前端可以接管最终位置。Form 与 Collection 的 AMIS submit API 只映射当前 View 中实际可 `editable`/`creatable` 的字段,readonly 展示数据不会被表单整体回传,后端仍保持“提交不可写字段即 422”的严格语义。静态首页禁止缓存,带内容 hash 的 `/assets/` 和带 AMIS 包版本号的 `/vendor/amis/<version>/` 使用长期 immutable 缓存。升级 AMIS 时 URL 会随版本变化,不会让 Edge 等浏览器继续复用旧 SDK;旧 `index.html` 也不会再引用已经不存在的动态 chunk。
## 后端分层
`backend/library` 是库级协议层,只有头文件,不依赖也不链接任何具体 JSON、HTTP Server、枚举反射或结构体反射实现。它依赖项目内的 `third_party/Structive`,由 Structive 负责 intrinsic property capability、Schema 和 synchronizationAdminive 自身包含 descriptor、JSON 适配协议、AMIS schema、状态协议、managed adapter 和抽象 `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;
```
源码树内置 nlohmann/json、magic_enum 和 cpp-httplibBoost.PFR 继续使用外部头文件。测试环境没有可发现的 `Boost::headers` CMake target 时,通过 `-DADMINIVE_BOOST_PFR_INCLUDE_DIR=<include>` 指向包含 `boost/pfr.hpp` 的目录。缺少该依赖时配置阶段会明确失败,避免适配器测试被静默跳过。
### 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);
};
```
如果运行时对象已经拥有权威状态和并发策略,不要为了 Adminive 再造一份完整配置快照。`field(name, label, accessor)` 直接接受 Structive `Property_Accessor`;默认数据成员仍使用 `structive::Member_Accessor`,方法型或子控件属性可使用 Structive 的 getter/setter accessor
```cpp
template <>
struct adminive::Type_Descriptor<Runtime_Control> {
static auto get() {
auto accessor = structive::trusted_callable_accessor<Runtime_Control>(
[](const Runtime_Control& value) { return value.setting(); },
[](Runtime_Control& value, int setting) {
value.apply_setting(setting);
});
return adminive::object<Runtime_Control>(
"runtime_control",
adminive::field("setting", "Setting", std::move(accessor))
.editable()
.unsynchronized());
}
};
```
这种直接描述不要求对象可复制,也不经过 `Object_Adapter``.unsynchronized()` 只表示 Adminive/Structive 不增加同步域;访问器调用的业务方法及对象自身同步仍是唯一权威实现。
`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` 绑定普通对象时继续使用调用方提供的 `Basic_Lock`;绑定 `Managed_Value<T>``Managed_Field<...>` 时直接使用 Structive 的同步拓扑。一次更新的候选快照、补丁应用、`prepare`、运行时 `commit`、外部 `commit`、失败恢复和响应快照都发生在同一个 managed write 作用域内。Adminive 不再维护第二套根 barrier/字段锁,也不再提供 `Resource_Lock_Scope`
```cpp
adminive::Managed_Value<Application_Config> config;
auto radio = config.member<&Application_Config::radio_service>();
adminive::Resource_Service<Radio_Service_Config, Json> resource(
radio,
"/config/radio",
transaction
);
```
事务回调应保持短小。如果持久化事务需要整个根对象的一致性快照,应在该 `Managed_Value` 实例上使用 Structive 的共享同步拓扑,让所有 writable 根属性落入同一个 lock domain。示例 `Config_Store` 正是这样配置:文件持久化横跨整个 `Application_Config`,因此使用 `sync_all_shared`;普通资源默认仍使用独立属性锁,不会为了修改一个无关字段锁住所有可写字段。
### Structive managed property 与同步
Adminive 不再拥有独立的属性同步系统。字段描述只声明这个字段在 Adminive managed model 中是否可写以及是否需要同步,实际 property capability、lock slot、mutex 和 guard 都由 `third_party/Structive` 实现。
字段默认是 intrinsic read-only
```cpp
return adminive::object<Config>(
"config",
ADMINIVE_FIELD(Config, immutable_value),
ADMINIVE_FIELD(Config, editable_value).editable(),
ADMINIVE_FIELD(Config, backend_mutable_value).read_write(),
ADMINIVE_FIELD(Config, externally_synchronized_value).read_write().unsynchronized()
);
```
常用语义如下:
- 普通 `ADMINIVE_FIELD(...)`Structive intrinsic read-only。不会创建 lock slot,不贡献 mutexmanaged read 直接走 Structive 的零锁路径。
- `.editable()`:字段 intrinsic read-write,同时允许前端 update。默认进入 Structive synchronization topology。
- `.creatable()`:字段 intrinsic read-write,同时允许 create 输入。
- `.read_write()`:字段 intrinsic read-write,但不因此对前端开放编辑;适合后台代码、配置加载或其他受控路径修改。
- `.unsynchronized()`:保持字段 intrinsic writable,但明确不为它创建 Structive mutex;仅应在字段自身原子化、外部已有更高层同步或生命周期保证单线程时使用。
`readable()/editable()/creatable()/sensitive()` 等仍是 Adminive 的领域/UI 元数据,不是 Structive 的访问控制。Structive 不决定 HTTP、GUI、RPC 或其他外部系统是否应该暴露某字段;Adminive 的具体适配器根据自己的元数据和业务场景决定。
`Managed_Value<T>` 是 Structive `Property_Object` 的 Adminive 适配对象,`Managed_Field` 是一个轻量字段路径代理:
```cpp
adminive::Managed_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 radio_snapshot = radio.snapshot();
auto root_snapshot = config.snapshot();
```
反射字段同样使用 `field<Index>()`
```cpp
adminive::Managed_Value<Pfr_Config> reflected_config;
reflected_config.field<0>().write([](auto& value) {
value = 8;
});
```
只读字段的优化来自 Structive 本身,而不是 Adminive 的运行时判断。对于 intrinsic read-only 存储属性,解析后的 slot 为 `unsynchronized_slot`,它不会增加 `lock_count`,也不会创建 mutex;因此“给字段添加 read-only 信息”会直接改变对象的同步布局和热路径成本。嵌套路径同样利用这条信息:如果父字段自身不是 writable,读取其中的 intrinsic read-only 子字段不会因为同级存在可写字段而获取父锁;`.unsynchronized()` 子字段的 managed write 也不会获取该父锁。如果某个祖先字段自身声明为 writable,则它允许整体替换整个子树,该祖先的同步语义自然覆盖后代访问。直接绕过 managed API 修改原始成员属于 raw C++ path,同时也绕过 Structive 的同步保证。
`Managed_Value::read/write``Managed_Field::read/write` 的回调结果不能把受保护对象的引用生命周期带出同步作用域。接口在编译期拒绝引用、指针、`reference_wrapper` 和 ranges view 返回值;需要把数据带出时应返回值副本或使用 `snapshot()`
默认 writable 字段使用独立同步域。Adminive 的 `.unsynchronized()` 会把对应 Structive property 明确设为无同步;需要跨多个根属性保持一致性的特殊实例,可以直接传入 Structive `Property_Synchronization`
```cpp
auto synchronization = structive::property_synchronization(
structive::Synchronization_Plan{structive::Synchronization_Default::shared}
);
adminive::Managed_Value<Application_Config> config(std::move(synchronization));
```
这也是 `Config_Store` 的选择,因为一次文件提交需要读取完整根配置。对于一般内存对象不应无条件使用 shared topology,否则会把本来可以并发的 writable 字段收敛到同一把锁。
如果对象生命周期已经由外部严格保证为单线程,可以使用 Structive 的 `No_Lock_Policy`
```cpp
adminive::Managed_Value<Application_Config, structive::No_Lock_Policy> config;
```
如果需要使用自定义 shared-lockable mutex,则使用 Adminive 的薄适配器:
```cpp
adminive::Managed_Value<Config, adminive::Mutex_Policy<My_Shared_Mutex>> config;
```
描述协议 `adminive.resource` 当前版本为 5。字段输出 `writable` 与最终 `synchronized` 信息,不再包含旧的 `lock_mode``writable` 表示 Adminive managed model 的 intrinsic mutability`synchronized` 只表示该 writable 字段是否贡献 Structive 同步域,与前端是否 editable 是两个独立维度。
数据库事务不能替代运行时同步。数据库负责持久化原子性;Structive synchronization 负责当前进程中的 mutable consistency`Resource_Transaction` 负责 Adminive 更新过程中外部副作用的 prepare/commit/rollback。三者职责保持独立。
### 外部控件适配
复杂业务类型通过 `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();
```
敏感字段不会进入前端数据,也不会进入带默认值的描述器。
### Httplib Request Context
`Http_Resource::bind(server)` 保持原语义,未提供额外选项时事务收到空 `Request_Context`。需要把认证用户、request id、remote address 或 transport metadata 传入 `Resource_Transaction` 时,使用 `Httplib_Bind_Options`
```cpp
adminive::Httplib_Bind_Options options;
options.context_factory = [](const httplib::Request& request) {
adminive::Request_Context context;
context.user = request.get_header_value("X-User");
context.request_id = request.get_header_value("X-Request-Id");
context.remote_address = request.remote_addr;
context.attributes.emplace("transport", "httplib");
return context;
};
resource.bind(server, std::move(options));
```
这个 Context 只在最外层 HTTP bridge 构造,`Resource_Service` 和 transaction 内层不重复检查 transport。`Http_Status_Resource<Status, Json>` 与 Drogon 的状态资源保持同一协议,注册 `/descriptor``/amis``/data`,状态 reader 同样可以读取 `Request_Context`。Context factory 或状态 reader 抛出的异常在 transport 边界转换成结构化 HTTP 500,不允许异常穿过 HTTP 回调。真实 `Adminive_Httplib_Adapter_Test` 会绑定 localhost 临时端口并通过 cpp-httplib Client 验证 descriptor/view/data/AMIS、Context 传递、独立 Status Resource、CRUD、paging、sort/search/filter、非法 query、row reorder 和 item status。
### 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 协议。两个 transport 都把除 `page/perPage/orderBy/orderDir` 之外的动态 query 字段交给 `Collection_Service`,因此未被 View Schema 声明为 searchable/filterable 的字段会统一返回 400,不能在某个 adapter 中静默忽略。当前 `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`,只作为完整示例实现,不属于公共库头文件。
## 字段呈现与表格列是两层
Field Descriptor 只描述字段自身和单字段呈现:
```cpp
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
.creatable()
.editable()
.required()
.select_input();
```
表格列属于 `Table_View`,列顺序就是 `table_view()` 中的声明顺序,排序、搜索、筛选和固定列能力也在这里声明:
```cpp
template <>
struct adminive::Type_View_Descriptor<Radio_State> {
static adminive::Table_View table() {
using T = Radio_State;
return adminive::table_view<T>(
adminive::column<&T::mode>("Mode").sort().filter(),
adminive::column<&T::port>("Port").sort().search().fix(adminive::Table_Fixed::left),
adminive::column<&T::buffer_count>("Buffers").sort()
).default_sort("buffer_count");
}
};
```
- Field Descriptor 的 `name` 是稳定的数据字段名。
- `Field_Presentation::label/control/options` 描述一个字段单独出现时如何呈现。
- `Table_View` 决定哪些字段进入表格、列顺序、列标题、固定位置以及可排序/搜索/筛选能力。
- Collection Service 只接受 `Table_View` 已声明的查询字段,因此 View Schema 同时是前端语义和后端能力契约。
## 枚举列表项
```cpp
enum class Radio_Mode {
receive,
transmit,
duplex,
maintenance
};
```
业务结构直接使用枚举:
```cpp
struct Radio_State {
Radio_Mode mode{Radio_Mode::receive};
};
```
描述中不需要手写枚举值集合,字段只声明自己的呈现;表格能力由 `Table_View` 另行决定:
```cpp
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
.creatable()
.editable()
.required()
.select_input();
```
示例通过可选的 `magic_enum` 桥接生成;核心只依赖 `Enum_Adapter`
```json
{
"name": "mode",
"value_type": "enum",
"presentation": {
"label": "Operating Mode",
"control": "select",
"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()
.date_input();
ADMINIVE_FIELD_LABEL(T, accent_color, "Accent Color")
.editable()
.required()
.color_input();
```
日期控件自动补充:
```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/view
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/view
GET /admin/config/radio/data
GET /admin/config/radio/amis
POST /admin/config/radio/data
GET /admin/config/alerts/descriptor
GET /admin/config/alerts/view
GET /admin/config/alerts/data
GET /admin/config/alerts/amis
POST /admin/config/alerts/data
```
## 前端
默认前端仍然只需要读取 `/admin/amis` 并渲染最终页面;`/descriptor``/view``/data` 是稳定的后端语义接口,供自定义前端、调试器或其他 Adapter 直接消费。业务字段名单、表格列和组合关系都不在前端硬编码。
创建外部 `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
npm --prefix .\frontend audit --omit=dev
```
发布前必须审查生产依赖报告:优先消除直接依赖和 critical 项;上游暂无修复的传递项要记录来源与暴露面,不能用 `npm audit fix --force` 做未经验证的跨主版本升级。`package.json` 的 override 只用于不进入浏览器运行时、且已通过正常 `npm ci`、production build 和 E2E 验证的传递依赖修复。
## 后端
源码构建会优先使用 `third_party/Structive` 内嵌目录;如果两个库在同一父目录下独立维护,则自动使用同级 `../Structive`。其他布局通过 `-DADMINIVE_STRUCTIVE_SOURCE_DIR=<path>` 显式指定,不需要复制第二份 Structive 源码。
```powershell
cmake -S . -B build -G Ninja
cmake --build build
ctest --test-dir build --output-on-failure
.\build\backend\Adminive_Server.exe 9999
```
测试使用 `backend/test_support/adminive_test.hpp``ADMINIVE_CHECK`,不会像标准 `assert()` 一样在 Release `NDEBUG` 下被移除。旧的 `Synchronized_Value`/`Resource_Lock_Scope` 测试实现已经删除;仍有意义的嵌套同步、无锁字段、读写能力和多线程序列化用例由当前 `Managed_Value` 测试覆盖,不恢复旧 API。
核心测试各自保护一层边界:`Core_Adapter` 使用非 nlohmann 的最小 JSON 实现验证 Core 真正与第三方解耦;`Managed` 验证 Structive topology、回调生命周期和并发;`View_Schema` 验证后端字段组合、固定列/查询授权、Manifest 与 AMIS 翻译;`Safety`/`Advanced_Adapter` 验证输入、敏感字段、Object/Value/Control/Polymorphic/Reflection Adapter 和事务恢复;Httplib/Drogon 测 transport 一致性;Gallery 只测示例注册表与真实端点,不重复 Core 算法。新增用例应放进拥有该契约的文件,不再扩张单一大杂烩测试。
CTest 使用 label 区分 `unit``protocol``http``install``package``header``concurrency``managed` 等测试。全部 Adminive Core 公共头和 5 个 Service Adapter 公共头都注册独立 include 编译测试,避免 umbrella header 掩盖缺失依赖。`Adminive_Install_Consumer_Test` 会先安装到构建目录内的固定测试 prefix,再从独立 `install_consumer` 工程只通过 `find_package(Adminive)` 同时构建 `Adminive::Core``Adminive::Default`/Httplib consumer。安装包不再把仓库内置的 nlohmann/json、magic_enum、cpp-httplib 复制到公共 include 命名空间;测试会用独立依赖 package fixture 验证导出目标确实通过 `find_dependency` 解析外部依赖,并检查安装 prefix 中不存在这些 bundled header。`Adminive_Package_Zip_Test` 会实际生成源码 ZIP,验证根目录精确文件匹配、release/license metadata、无残留嵌套工程且已删除的旧同步实现不会重新进入发布包。
Clang/GNU sanitizer
```powershell
cmake -S . -B verification/asan-ubsan -DADMINIVE_ENABLE_ASAN=ON -DADMINIVE_ENABLE_UBSAN=ON
cmake -S . -B verification/tsan -DADMINIVE_ENABLE_TSAN=ON
```
MSVC 只启用其实际支持的 ASan:
```powershell
cmake -S . -B verification/asan -DADMINIVE_ENABLE_ASAN=ON
```
MSVC 配置 UBSan 或 TSan 会在 CMake 配置阶段明确失败,不允许出现“没有启用 sanitizer 但 Gate 显示通过”的假结果。完整 Gate 直接执行:
```powershell
python .\scripts\verify.py
```
正式发布环境安装了真实 Drogon 与 nlohmann_json CMake package 时,再执行:
```powershell
python .\scripts\verify.py --real-drogon
```
这个选项会在 Release 构建中启用真实 Drogon adapter target 和真实 installed-package consumer;默认 fake Drogon contract test 不冒充这一 Gate。
Gate 使用固定 `verification/debug``verification/release``verification/asan-ubsan``verification/tsan` 路径。Clang/GNU 运行 Debug/Release 全构建和 CTest、ASan+UBSan Core/Service 矩阵、TSan concurrency、前端依赖/单测/production build、Playwright Chromium E2EWindows MSVC 只运行真实 ASan,并明确输出 UBSan/TSan SKIP,额外运行 `msedge` channel。Sanitizer 矩阵覆盖 Core Adapter、Advanced Adapter、Safety、Managed、View Schema、Drogon 和真实 Httplib,不把耗时的示例 Gallery 重复做 sanitizer 构建。前端 E2E 覆盖导航、同一 Manifest 五种布局、display/create/edit、Collection CRUD/分页/排序/搜索/筛选/状态/重排/错误、422 nested validation、Sensitive 输出过滤、Object Adapter transaction counter、Transaction rollback、状态轮询序列、协议边界和缓存头。需要把前端测试注册进 CTest 时配置 `ADMINIVE_ENABLE_FRONTEND_TESTS=ON`
安装并通过 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)
```
安装后的 Adapter component 使用外部 CMake package`Nlohmann` 解析 `nlohmann_json::nlohmann_json``MagicEnum` 解析 `magic_enum::magic_enum``Httplib` 解析 `httplib::httplib``BoostPfr` 解析 `Boost::headers``Drogon` 解析 `Drogon::Drogon`。仓库内置的前三个 header 只服务源码树 build/test,不安装到消费者的通用 include 目录。完整规则见 `RELEASE.md`
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
```
`ADMINIVE_STRUCTIVE_SOURCE_DIR` 指向同级外部源码树时,打包目标只把 Structive 发布清单中的源码、文档、测试和 Codex 约束暂存到归档内的 `third_party/Structive`;不会复制 `.git`、验证构建目录或在工作树中制造第二份依赖。内嵌布局继续直接按同一归档路径打包。
压缩函数依次接收目标名、输出文件、根目录、包含列表变量名和排除列表变量名。只扫描包含列表指定的路径,再按相对于根目录的规则排除:
```cmake
add_project_zip_target(
package_zip
"${CMAKE_CURRENT_LIST_DIR}/Adminive.zip"
"${CMAKE_CURRENT_LIST_DIR}"
adminive_package_includes
adminive_package_excludes
)
```
## Release and third-party policy
See `RELEASE.md` for source-tree versus installed-package dependency rules and the real Drogon release gate. Redistributed dependency notices and license texts are listed in `THIRD_PARTY_NOTICES.md`. The repository does not currently declare an outbound license for Adminive/Structive project-owned code.