# 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 类型: ```cpp auto descriptor = adminive::to_descriptor_json(); auto encoded = adminive::to_json(config); auto result = adminive::apply_frontend_patch(config, input); ``` ### 外部值类型适配 外部包装类型不需要继承 Adminive 类型。可以针对全部 JSON 实现提供通用适配,也可以只针对某个 JSON 类型特化: ```cpp template struct adminive::Value_Adapter { 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`。`adminive/adapters/magic_enum.hpp` 是可选桥接,也可以为单个枚举自行实现: ```cpp template <> struct adminive::Enum_Adapter { static std::vector values(); static std::string_view name(My_Mode value); static std::optional cast(std::string_view value); }; ``` ### PFR 或其他结构体反射适配 核心通过 `Reflection_Adapter` 接收字段数量、字段名称和按索引访问。可选桥接头为: ```cpp #include "adminive/adapters/boost_pfr.hpp" ``` 普通聚合类型: ```cpp template <> struct adminive::Reflection_Adapter : adminive::Boost_Pfr_Reflection_Adapter {}; ``` 由多个数据基类组成的配置类型可以展开所有基类字段: ```cpp template <> struct adminive::Reflection_Adapter : adminive::Boost_Pfr_Base_Reflection_Adapter< Mode_ACS_Config, Mode_ACS_Config_Base_Data, Mode_ACS_Config_Data> {}; ``` 较旧 Boost.PFR 不提供字段名时,外部为每个数据基类特化 `Boost_Pfr_Name_Adapter`;较新版本自动使用 `boost::pfr::get_name()`。 ### 不可复制运行时对象 `Object_Adapter` 将不可复制、含原子字段或带运行时状态的对象映射到纯值配置模型。读取只要求 `snapshot()`,编辑要求 `snapshot()` 和 `commit()`,创建能力才额外要求 `create()`: ```cpp template <> struct adminive::Object_Adapter { 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::commit()`提交运行时模型。外部持久化和系统副作用通过 `Resource_Transaction` 分成准备、提交和回滚三个阶段: ```cpp adminive::Resource_Transaction 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 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 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", 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` 内部同时维护根对象 barrier 和层级字段锁。所有字段读取至少持有根共享 barrier,保证它不会和整对象提交、替换或序列化事务并发;普通只读字段不再额外获取字段锁。可编辑或显式 `.locked()` 的字段再获取对应的层级字段锁,因此不同动态字段仍可以并发。`.locked(false)` 只关闭当前字段自己的专用字段锁;如果该字段是结构体且后代存在动态字段,后代所需的结构 barrier 仍然保留。对未启用字段锁的字段执行写操作时会直接升级为根独占操作,避免出现无保护写入。序列化、反序列化、对象级校验和整对象事务属于一致性操作,会独占被操作对象的 barrier,避免跨字段撕裂快照。嵌套对象序列化只独占该子对象节点,同时持有祖先共享锁,不会无条件锁死整个根配置。 ```cpp adminive::Synchronized_Value 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 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 config; adminive::Resource_Service resource(config, "/config"); ``` 同步对象不公开原始值和 mutex。普通业务代码只能通过 `read()`、`write()`、`snapshot()`、`replace()`、显式成员的 `member<&T::x>()` 和反射字段的 `field()` 进入同步协议,避免重新出现 `data() + mutex()` 由调用方手工配对的问题。同步回调会静态拒绝引用、指针、`reference_wrapper` 和 ranges view 等常见引用逃逸形式;自定义返回类型如果内部保存受保护对象的地址或引用,仍由调用方保证不会把它带出锁作用域。 ### 外部控件适配 复杂业务类型通过 `Control_Adapter` 明确决定表单控件和列表列。框架不会把 `vector` 或 `map` 隐式决定为表格、列表、分页或标签页;未配置控件的结构类型会在 Schema 生成时给出明确错误。 ```cpp template <> struct adminive::Control_Adapter { static Json make_control(const Json& field, adminive::Control_Context context); static Json make_column(const Json& field); }; ``` ### 多态对象 `Polymorphic_Adapter` 返回带真实 C++ 类型的 variant 元组。Adminive 因此能继续调用每个派生类型字段的 `Control_Adapter`,不会退回无类型的 JSON 控件路径。 ```cpp template <> struct adminive::Polymorphic_Adapter { 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", "串口设备"), adminive::polymorphic_variant("network", "网络设备") ); } static Json encode(const Device& value); static void decode(Device& target, const Json& value, adminive::Write_Context context); }; ``` `decode_polymorphic_alternative()` 在判别类型不变时执行补丁更新并保留未提交字段;切换类型时通过该类型的 `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(); ``` 敏感字段使用: ```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 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` 注册 `/descriptor`、`/amis` 和 `/data`。列表 CRUD 使用 `Drogon_Collection_Resource`,和 httplib 的 `Http_Collection_Resource` 共用纯头文件 `Collection_Service`;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 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 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`、`std::optional`、`std::optional`、`std::optional` 和可适配枚举使用相同基础控件并自动启用清空;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 operating_state; Status_Value refresh_sequence; Status_Value 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 ) ```