2026-08-07 09:25:55 +08:00
2026-08-07 09:25:55 +08:00
2026-08-07 09:25:55 +08:00
2026-08-06 16:48:57 +08:00
2026-08-06 16:01:04 +08:00
2026-08-06 16:01:04 +08:00
2026-08-06 22:38:23 +08:00
2026-08-07 09:25:55 +08:00

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

target_link_libraries(your_target PRIVATE Adminive::Core)

Adminive::HttpAdminive::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.hppmagic_enum.hppboost_pfr.hpphttplib.hppdrogon.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_nameappend_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 对一次更新持有同一个一致性作用域,锁覆盖快照、补丁应用、prepare、运行时 commit、外部 commit、失败恢复和响应快照。绑定根 Synchronized_Value 时独占根 barrier;绑定 Synchronized_Field 子对象时默认只独占对应子对象节点并持有祖先共享 barrier,不会为了修改一个子配置锁死整个根配置。如果事务回调需要读取、复制或序列化整个根配置,则必须把资源作用域设为 Resource_Lock_Scope::root,让整个事务持有根独占 barrier。事务回调应保持短小;使用非递归锁时,回调不能再次进入同一个同步作用域,而应调用已经处于锁内的持久化实现。

auto radio = config.member<&Application_Config::radio_service>();
adminive::Resource_Service<Radio_Service_Config, Json> resource(
    radio,
    "/config/radio",
    transaction,
    adminive::Resource_Lock_Scope::root
);

local 是子资源默认值,适用于事务只操作该子对象或对应数据库行的场景;root 只用于事务确实需要根对象一致性快照的场景,例如把整个 Application_Config 写入一个 JSON 配置文件。

配置同步协议

持久化和进程内同步是两个独立问题。配置写入文件或数据库后,HTTP 线程、业务线程和后台任务仍可能同时访问运行时对象,因此动态字段仍需要进程内同步。Adminive 的默认策略是:.editable() 字段自动拥有字段锁;普通只读字段不拥有字段锁;.locked() 可以强制给不可编辑但会被程序动态修改的字段启用字段锁;.locked(false) 可以显式关闭某个可编辑字段的字段锁。

return adminive::object<Config>(
    "config",
    ADMINIVE_FIELD(Config, editable_value).editable(),
    ADMINIVE_FIELD(Config, startup_only_value),
    ADMINIVE_FIELD(Config, backend_mutable_value).locked(),
    ADMINIVE_FIELD(Config, no_dedicated_field_lock).editable().locked(false)
);

Synchronized_Value<T, Object_Lock, Field_Lock> 内部同时维护根对象 barrier 和层级字段锁。所有字段读取至少持有根共享 barrier,保证它不会和整对象提交、替换或序列化事务并发;普通只读字段不再额外获取字段锁。可编辑或显式 .locked() 的字段再获取对应的层级字段锁,因此不同动态字段仍可以并发。.locked(false) 只关闭当前字段自己的专用字段锁;如果该字段是结构体且后代存在动态字段,后代所需的结构 barrier 仍然保留。对未启用字段锁的字段执行写操作时会直接升级为根独占操作,避免出现无保护写入。序列化、反序列化、对象级校验和整对象事务属于一致性操作,会独占被操作对象的 barrier,避免跨字段撕裂快照。嵌套对象序列化只独占该子对象节点,同时持有祖先共享锁,不会无条件锁死整个根配置。

adminive::Synchronized_Value<Application_Config> config;
auto radio = config.member<&Application_Config::radio_service>();
auto worker_count = radio.member<&Radio_Service_Config::worker_count>();
worker_count.write([](auto& value) {
    value = 8;
});
auto snapshot = radio.snapshot();
auto root_snapshot = config.snapshot();

adminive::Synchronized_Value<Pfr_Config> reflected_config;
reflected_config.field<0>().write([](auto& value) {
    value = 8;
});

描述协议 adminive.resource 版本为 3。每个字段会输出 lock_mode 和最终计算后的 synchronized,因此服务层能够明确知道该字段的并发语义,而不是由具体 JSON 或 HTTP 后端隐式决定。

数据库事务不能替代这套运行时锁。Resource_Service 在一次 HTTP 更新中持有对应资源的一致性锁作用域,覆盖候选快照、补丁应用、对象校验、运行时提交、持久化回调、失败回滚和响应快照。数据库负责持久化原子性;对象/字段 barrier 负责当前进程中的运行时一致性。

如果对象生命周期已经由外部严格保证为单线程,可以统一换成 Empty_Lock,不需要维护另一套无锁实现:

adminive::Synchronized_Value<Application_Config, adminive::Empty_Lock> config;
adminive::Resource_Service<Application_Config, Json, adminive::Empty_Lock> resource(config, "/config");

同步对象不公开原始值和 mutex。普通业务代码只能通过 read()write()snapshot()replace()、显式成员的 member<&T::x>() 和反射字段的 field<Index>() 进入同步协议,避免重新出现 data() + mutex() 由调用方手工配对的问题。同步回调会静态拒绝引用、指针、reference_wrapper 和 ranges view 等常见引用逃逸形式;自定义返回类型如果内部保存受保护对象的地址或引用,仍由调用方保证不会把它带出锁作用域。

外部控件适配

复杂业务类型通过 Control_Adapter<T, Json> 明确决定表单控件和列表列。框架不会把 vectormap 隐式决定为表格、列表、分页或标签页;未配置控件的结构类型会在 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,并同时返回 errorsfield_errors

描述器校验和敏感字段

描述器生成时会拒绝空字段名、重复字段名以及不存在或不可排序的默认排序字段。普通描述器默认不输出字段默认值;需要默认值时显式调用:

auto descriptor = adminive::to_descriptor_json_with_defaults<Json, Config>();

敏感字段使用:

ADMINIVE_FIELD(T, password).sensitive().editable();

敏感字段不会进入前端数据,也不会进入带默认值的描述器。

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,只作为完整示例实现,不属于公共库头文件。

列字段名称、显示名称和排序

ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
    .creatable()
    .editable()
    .required()
    .list_label("Mode")
    .order(5)
    .sortable();
  • name 来自成员名称,用于 JSON 字段和接口参数。
  • label 用于表单标签。
  • list_label 用于列表表头,未设置时回退到 label
  • order 控制列表列顺序,未设置时按字段声明顺序生成默认值。
  • sortable 控制该列能否排序,后端只接受已声明为可排序的字段。

枚举列表项

enum class Radio_Mode {
    receive,
    transmit,
    duplex,
    maintenance
};

业务结构直接使用枚举:

struct Radio_State {
    Radio_Mode mode{Radio_Mode::receive};
};

描述中不需要手写选项:

ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
    .creatable()
    .editable()
    .required()
    .list_label("Mode")
    .order(5)
    .sortable();

示例通过可选的 magic_enum 桥接生成;核心只依赖 Enum_Adapter

{
  "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"

树状配置

树结构由嵌套的已描述对象表达,不额外维护一份前端树配置:

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::nulloptstd::string_view 只允许作为只读存储。

日期和颜色通过后端字段描述指定:

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");

日期控件自动补充:

{
  "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/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 相对符号链接:

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

安装并通过 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
)
S
Description
后端元数据驱动的通用管理界面框架
Readme 2.3 MiB
Languages
C++ 81.2%
CMake 7.8%
TypeScript 7%
CSS 2.2%
Python 1.1%
Other 0.6%