38 KiB
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++ 结构直接驱动后台管理能力,而不是再建立一套独立的数据模型。
整体职责按语义分层:
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/mutex,managed 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。
后端 UI 的三层描述与组合层
一个字段“是什么”、一个字段“怎么呈现”、一组字段“怎么组成业务视图”,以及多个业务视图“怎么组成页面”是四个不同问题。Adminive 不再把它们塞进同一个 Field Descriptor:
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 也不承担业务组合决策。
例如一个配置对象可以只定义字段事实和单字段控件:
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();
同一个类型的编辑视图再独立决定组合:
return edit_form<T>(vertical(
group("Network", horizontal(use<&T::host>(), use<&T::port>())),
card("Appearance", use<&T::color>())
));
如果同一个类型还需要集合展示,先定义统一 Collection View,再只切换展示 mode:
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:
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 Contract,Contract 明确组件类型以及 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 都执行真实 HTTP;column reorder 是纯前端表现状态,因此实验台明确展示后端 column_reorderable 授权和本地列顺序变化,而不会伪造业务提交。协议边界页每次都会重新执行 grid(0)、非法 tab、缺失/重复 Slot Contract、重复 slot、readonly 提交、nested 多字段 422、object validator、多态 discriminator 和非法 Collection query。
画廊资源同时提供正常 Adminive HTTP 接口:
/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/lab/invalid-filter
/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 和 synchronization;Adminive 自身包含 descriptor、JSON 适配协议、AMIS schema、状态协议、managed adapter 和抽象 Resource_Service:
target_link_libraries(your_target PRIVATE Adminive::Core)
Adminive::Http 与 Adminive::Core 指向同一套纯头文件协议层,只用于表达消费侧语义,不额外引入依赖。
backend/service 是服务桥接层,放置具体适配器、第三方头文件和示例服务。需要什么桥接就显式选择什么目标:
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 都属于服务桥接层。核心协议不会自动包含它们。项目默认示例使用:
#include "adminive/adapters/nlohmann_json.hpp"
#include "adminive/adapters/magic_enum.hpp"
using Json = nlohmann::json;
JSON 适配
外部 JSON 类型通过 Json_Adapter<Json> 接入。适配器负责对象、数组、标量、字段访问、解析和输出。所有转换入口显式携带 JSON 类型:
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 类型特化:
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 是可选桥接,也可以为单个枚举自行实现:
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> 接收字段数量、字段名称和按索引访问。可选桥接头为:
#include "adminive/adapters/boost_pfr.hpp"
普通聚合类型:
template <>
struct adminive::Reflection_Adapter<Config>
: adminive::Boost_Pfr_Reflection_Adapter<Config> {};
由多个数据基类组成的配置类型可以展开所有基类字段:
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():
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> 分成准备、提交和回滚三个阶段:
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。
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:
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,不贡献 mutex,managed 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 是一个轻量字段路径代理:
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>():
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:
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:
adminive::Managed_Value<Application_Config, structive::No_Lock_Policy> config;
如果需要使用自定义 shared-lockable mutex,则使用 Adminive 的薄适配器:
adminive::Managed_Value<Config, adminive::Mutex_Policy<My_Shared_Mutex>> config;
描述协议 adminive.resource 当前版本为 4。字段输出 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 生成时给出明确错误。
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 控件路径。
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。
描述器校验和敏感字段
描述器生成时会拒绝空字段名、重复字段名以及不存在或不可排序的默认排序字段。普通描述器默认不输出字段默认值;需要默认值时显式调用:
auto descriptor = adminive::to_descriptor_json_with_defaults<Json, Config>();
敏感字段使用:
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:
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。真实 Adminive_Httplib_Adapter_Test 会绑定 localhost 临时端口并通过 cpp-httplib Client 验证 descriptor/view/data/AMIS、Context 传递、CRUD、paging、sort/search/filter、非法 query、row reorder 和 item status。
Drogon 生命周期和异步提交
Drogon_Resource 的路由回调捕获共享的 Resource_Service,绑定完成后销毁 binder 不会留下悬空回调。Drogon_Bind_Options 同时配置异步执行器、请求上下文和 Drogon Filter:
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 描述器生成,不再包含固定的服务名、字段名或模板。服务标题、字段标签、颜色和轮询周期由调用端传入的状态描述器决定。
目录结构
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 只描述字段自身和单字段呈现:
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
.creatable()
.editable()
.required()
.select_input();
表格列属于 Table_View,列顺序就是 table_view() 中的声明顺序,排序、搜索、筛选和固定列能力也在这里声明:
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 同时是前端语义和后端能力契约。
枚举列表项
enum class Radio_Mode {
receive,
transmit,
duplex,
maintenance
};
业务结构直接使用枚举:
struct Radio_State {
Radio_Mode mode{Radio_Mode::receive};
};
描述中不需要手写枚举值集合,字段只声明自己的呈现;表格能力由 Table_View 另行决定:
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
.creatable()
.editable()
.required()
.select_input();
示例通过可选的 magic_enum 桥接生成;核心只依赖 Enum_Adapter:
{
"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"。
树状配置
树结构由嵌套的已描述对象表达,不额外维护一份前端树配置:
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{};
};
界面结构为:
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,子字段使用点路径,例如:
primary_endpoint.host
appearance.effective_date
提交时仍然形成嵌套 JSON 对象。
配置联动
字段描述通过 visible_on 保存 amis 表达式:
ADMINIVE_FIELD_LABEL(T, backup_endpoint, "Backup Endpoint")
.editable()
.visible_on("${$self.mode == 'duplex'}");
当 mode 不是 duplex 时,整个 Backup Endpoint 子树隐藏。
第二个配置示例使用组合条件:
ADMINIVE_FIELD_LABEL(T, webhook_endpoint, "Webhook Endpoint")
.editable()
.visible_on("${$self.enabled && $self.channel == 'webhook'}");
只有启用告警且投递通道为 webhook 时,才显示 webhook 子配置。
$self 表示当前对象的数据域。顶层字段会解析为 ${mode ...},嵌套对象会自动补成 ${parent.mode ...},避免复用子配置描述器时引用到错误的数据域。
完整输入控件
后端根据 C++ 类型自动选择基础控件:
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 只允许作为只读存储。
日期和颜色通过后端字段描述指定:
ADMINIVE_FIELD_LABEL(T, effective_date, "Effective Date")
.editable()
.required()
.date_input();
ADMINIVE_FIELD_LABEL(T, accent_color, "Accent Color")
.editable()
.required()
.color_input();
日期控件自动补充:
{
"type": "input-date",
"valueFormat": "YYYY-MM-DD",
"displayFormat": "YYYY-MM-DD",
"clearable": true
}
一次性确认修改
配置表单不会在单个输入项改变时调用后端。所有修改先保留在表单数据域,点击 Confirm Changes 后一次性提交完整配置:
{
"actions": [
{"type": "reset", "label": "Reset"},
{"type": "submit", "label": "Confirm Changes", "level": "primary"}
]
}
后端先复制当前对象,在副本上完成所有字段赋值和校验,全部成功后才替换原对象,因此一次提交具有对象级事务语义。
两组配置示例
页面包含四个后端定义的标签页:
Radio State List
Radio Service Configuration
Alert Configuration
Device Tables
Radio Service Configuration 展示字符串、枚举、整数、浮点数、树状子配置、日期和颜色。
Alert Configuration 展示布尔开关、字符串、两个枚举、数字、日期、颜色和条件显示的 webhook 子配置。
对象状态
对象状态使用独立返回类型描述,不混入 CRUD 对象字段:
struct Radio_Item_Status {
Status_Value<Radio_Operating_State> operating_state;
Status_Value<std::uint64_t> refresh_sequence;
Status_Value<std::string> refresh_time;
};
状态值同时携带颜色:
{
"operating_state": {
"value": "running",
"color": "#16a34a"
}
}
资源通过成员函数指针注册:
resource.register_status<&Radio_State::status>(2000);
只有用户打开某一行的 View status PopOver 后,该行状态才开始请求和刷新。
API
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 相对符号链接:
pwsh -NoProfile -File .\scripts\link_node_modules.ps1
默认结构:
Adminive/
├── frontend/
│ └── node_modules -> ../node_modules
└── node_modules/
安装和构建:
npm --prefix .\frontend ci
npm --prefix .\frontend run build
后端
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。
CTest 使用 label 区分 unit、protocol、http、install、package、concurrency、managed 等测试。Adminive_Install_Consumer_Test 会先安装到构建目录内的固定测试 prefix,再从独立 install_consumer 工程只通过 find_package(Adminive) 配置、编译和运行,防止源码 include path 掩盖安装包错误。可选 sanitizer:
cmake -S . -B verification/asan-ubsan -DADMINIVE_ENABLE_ASAN=ON -DADMINIVE_ENABLE_UBSAN=ON
cmake -S . -B verification/tsan -DADMINIVE_ENABLE_TSAN=ON
完整 Gate 直接执行:
python .\scripts\verify.py
Gate 使用固定 verification/debug、verification/release、verification/asan-ubsan 和 verification/tsan 路径,依次完成 Debug/Release 全构建和 CTest、ASan+UBSan Core/Service 矩阵、TSan concurrency、前端依赖/单测/production build、Playwright Chromium E2E;Windows 额外运行 msedge channel。ASan+UBSan 矩阵覆盖 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 包使用:
cmake --install build --prefix D:/Adminive
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。
访问:
http://127.0.0.1:9999
源码压缩目标
cmake --build build --target package_zip
压缩函数依次接收目标名、输出文件、根目录、包含列表变量名和排除列表变量名。只扫描包含列表指定的路径,再按相对于根目录的规则排除:
add_project_zip_target(
package_zip
"${CMAKE_CURRENT_LIST_DIR}/Adminive.zip"
"${CMAKE_CURRENT_LIST_DIR}"
adminive_package_includes
adminive_package_excludes
)