# 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/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](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(vertical( group("Network", horizontal(use<&T::host>(), use<&T::port>())), card("Appearance", use<&T::color>()) )); ``` 如果同一个类型还需要集合展示,先定义统一 Collection View,再只切换展示 mode: ```cpp const auto base = collection_view( 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 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 接口: ```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//` 使用长期 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`: ```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-httplib;Boost.PFR 继续使用外部头文件。测试环境没有可发现的 `Boost::headers` CMake target 时,通过 `-DADMINIVE_BOOST_PFR_INCLUDE_DIR=` 指向包含 `boost/pfr.hpp` 的目录。缺少该依赖时配置阶段会明确失败,避免适配器测试被静默跳过。 ### 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); }; ``` 如果运行时对象已经拥有权威状态和并发策略,不要为了 Adminive 再造一份完整配置快照。`field(name, label, accessor)` 直接接受 Structive `Property_Accessor`;默认数据成员仍使用 `structive::Member_Accessor`,方法型或子控件属性可使用 Structive 的 getter/setter accessor: ```cpp template <> struct adminive::Type_Descriptor { static auto get() { auto accessor = structive::trusted_callable_accessor( [](const Runtime_Control& value) { return value.setting(); }, [](Runtime_Control& value, int setting) { value.apply_setting(setting); }); return adminive::object( "runtime_control", adminive::field("setting", "Setting", std::move(accessor)) .editable() .unsynchronized()); } }; ``` 这种直接描述不要求对象可复制,也不经过 `Object_Adapter`。`.unsynchronized()` 只表示 Adminive/Structive 不增加同步域;访问器调用的业务方法及对象自身同步仍是唯一权威实现。 `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` 绑定普通对象时继续使用调用方提供的 `Basic_Lock`;绑定 `Managed_Value` 或 `Managed_Field<...>` 时直接使用 Structive 的同步拓扑。一次更新的候选快照、补丁应用、`prepare`、运行时 `commit`、外部 `commit`、失败恢复和响应快照都发生在同一个 managed write 作用域内。Adminive 不再维护第二套根 barrier/字段锁,也不再提供 `Resource_Lock_Scope`。 ```cpp adminive::Managed_Value config; auto radio = config.member<&Application_Config::radio_service>(); adminive::Resource_Service 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", 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` 是 Structive `Property_Object` 的 Adminive 适配对象,`Managed_Field` 是一个轻量字段路径代理: ```cpp adminive::Managed_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 radio_snapshot = radio.snapshot(); auto root_snapshot = config.snapshot(); ``` 反射字段同样使用 `field()`: ```cpp adminive::Managed_Value 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 config(std::move(synchronization)); ``` 这也是 `Config_Store` 的选择,因为一次文件提交需要读取完整根配置。对于一般内存对象不应无条件使用 shared topology,否则会把本来可以并发的 writable 字段收敛到同一把锁。 如果对象生命周期已经由外部严格保证为单线程,可以使用 Structive 的 `No_Lock_Policy`: ```cpp adminive::Managed_Value config; ``` 如果需要使用自定义 shared-lockable mutex,则使用 Adminive 的薄适配器: ```cpp adminive::Managed_Value> 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` 明确决定表单控件和列表列。框架不会把 `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(); ``` 敏感字段不会进入前端数据,也不会进入带默认值的描述器。 ### 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` 与 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 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 协议。两个 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 { static adminive::Table_View table() { using T = Radio_State; return adminive::table_view( 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 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() .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 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/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=` 显式指定,不需要复制第二份 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 E2E;Windows 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.