# PROJECT KNOWLEDGE BASE
Rust 插件系统框架,提供 `Addon → Module → Action` 三层运行时分发架构。
API 通过字符串名称 `{addon}.{module}.{action}` 动态路由(如 `dms.task.table`),非编译时绑定。
## COMMANDS
```bash
# 构建
cargo build # 默认全 feature
cargo build --features "mysql" # 指定单个 feature
cargo build --no-default-features --features "mysql,cache"
# 测试
cargo test # 全部测试
cargo test -- --nocapture # 带 stdout 输出
cargo test create_plugin # 运行单个测试(按函数名匹配)
cargo test test_empty -- --nocapture # 单个测试带输出
# 检查(提交前必须通过)
cargo fmt -- --check
cargo clippy --all-targets --all-features -- -D warnings
# 发布
cargo package --list && cargo publish --dry-run && cargo publish
```
**Features**: `mysql` `sqlite` `mssql` `pgsql` `cache`(默认全启用)
## STRUCTURE
```
src/
├── lib.rs # Plugin trait, ApiResponse, ApiType, 全局状态
├── addon.rs # Addon trait + addon_create() 脚手架生成 + to_pascal_case()
├── module.rs # Module trait + DB helpers (db_find/db_select/db_insert/db_update/db_delete)
├── action.rs # Action trait + check() 参数验证 + Btn/BtnType/BtnColor/Dashboard UI组件
├── request.rs # Request struct + Method/ContentType 枚举
├── tools.rs # Tools 工具集(db/cache) + ToolsConfig
├── tables.rs # Tables 表格查询构建器(分页/排序/联表/树形) [feature-gated]
└── swagger.rs # Swagger OpenAPI 3.0 文档生成器(Swagger/Server/Api/RequestBody)
temp/ # 代码生成模板(action_table/action_add/action_del/action_get 等)
examples/addon/ # 完整 Addon→Module→Action 实现参考
tests/main.rs # 脚手架生成测试
```
## CODE STYLE
### 命名规范
| Struct/Enum/Trait | PascalCase | `ApiResponse`, `BtnType`, `DashboardModel` |
| 函数/方法 | snake_case | `module_name()`, `table_key()` |
| 常量/静态变量 | SCREAMING_SNAKE | `GLOBAL_DATA`, `PLUGIN_TOOLS` |
| 私有/内部方法 | 下划线前缀 | `_name()`, `_table_name()`, `_load_apis()` |
### 注释语言
本项目使用**中文注释**,保持一致性。用 `///` 写文档注释。
### Import 顺序
1. `crate::` 内部模块(按字母序)
2. 条件编译的外部 crate(`#[cfg(...)]` 的 `br_db`/`br_cache`)
3. 外部 crate(`json`, `lazy_static`, `log`, `serde`, `std`,按字母序)
```rust
use crate::action::Action;
use crate::addon::Addon;
use crate::module::Module;
#[cfg(any(feature = "mysql", feature = "sqlite", feature = "mssql", feature = "pgsql"))]
use br_db::Db;
use json::{array, object, JsonValue};
use std::collections::HashMap;
```
### Feature 条件编译(必须)
所有数据库相关代码必须用 feature gate 包裹:
```rust
#[cfg(any(feature = "mysql", feature = "sqlite", feature = "mssql", feature = "pgsql"))]
fn db_operation() { }
```
cache 用 `#[cfg(feature = "cache")]`。
### JSON 操作
使用 `json` crate(**非 serde_json**)。核心宏和类型:
```rust
use json::{object, array, JsonValue};
let obj = object! { "key" => "value", "num" => 42 }; // 创建对象
let arr = array!["a", "b"]; // 创建数组
obj["key"].as_str().unwrap_or(""); // 安全取值
obj.has_key("key"); // 检查 key
obj.is_empty() / obj.is_null(); // 空值判断
for (k, v) in obj.entries() { } // 遍历对象
for item in arr.members() { } // 遍历数组
```
### 错误处理
- Trait 方法返回 `Result<T, String>`,错误信息用中文
- API 层用 `ApiResponse::fail(code, "消息")` 或 `ApiResponse::error(data, "消息")`
- 错误码规范:`900_xxx` 系列为参数验证错误,`-1` 为通用错误
- 用 `.unwrap_or()` / `.unwrap_or_default()` 处理用户输入,**禁止裸 `.unwrap()`**
- `lock()` 可用 `.expect("描述")` 因为锁失败是不可恢复错误
```rust
// 正确
fn module(name: &str) -> Result<Box<dyn Module>, String> {
Err(format!("模型格式不正确: {name}"))
}
ApiResponse::fail(900_001, "参数错误")
// 错误
request[name].as_str().unwrap() // 禁止
```
### 类型名称推导
通过 `std::any::type_name::<Self>()` 解析模块路径自动推导名称:
- Action: `t[2].t[3].t[4]` → `addon.module.action`
- Module: `t[2].t[3]` → `addon.module`
- 表名: `{addon}_{module}` 自动推导
### `Box::leak` 模式
Module/Addon trait 中用 `Box::leak(s.into_boxed_str())` 返回 `&'static str`。
每个具体类型只泄漏一次,这是有意设计。新增类似方法时保持此模式并添加注释。
### Builder 模式
`Tables`、`Swagger`、`Btn`、`Dashboard` 均使用 builder 模式,方法返回 `&mut Self`:
```rust
self.table()
.main_table_fields(table_name, fields, hidd_field, show_field)
.search_fields(search_fields)
.params(request)
.get_table()
```
### Tables 完整用法参考(tables.rs)
`Tables` 是核心表格查询构建器,驱动所有 CRUD 列表视图(~287 个 table.rs + ~136 个 select.rs + 3 个 tree + 2 个 menu + 2 个 select_tree)。
#### 静态参数方法(用于 Action::params())
根据终端方法选择对应的 params 方法:
| `get_table()` | `Tables::params_table(object!{})` | page, limit, order, search, where_or, where_and, params |
| `get_tree()` | `Tables::params_table_tree(object!{})` | + pid, value, primary_key |
| `get_table_select()` | `Tables::params_table_select(object!{})` | + value, primary_key |
| `get_table_select_tree()` | `Tables::params_table_select_tree(object!{})` | + pid, value |
| `get_table_menu()` | `Tables::params_table(object!{})` | 同 get_table |
| `get_table_edit()` | `Tables::params_table(object!{})` | 同 get_table |
#### Builder 方法(返回 `&mut Self`)
| `main_table_fields` | `(table: &str, fields: JsonValue, hide: Vec<&str>, show: Vec<&str>)` | **必须首调**。设置主表名、字段定义、隐藏/显示白名单 |
| `main_select_fields` | `(table: &str, show_fields: Vec<&str>)` | select 专用。设置主表 + 显示字段(自动加 id) |
| `params` | `(request: JsonValue)` | **必须调用**。解析 page/limit/where_and/where_or/order/search/pid/primary_key/value |
| `search_fields` | `(fields: Vec<&str>)` | 搜索框匹配字段,自动拼接标题为 search_name |
| `filter_fields` | `(fields: Vec<&str>)` | 高级筛选字段(前端渲染筛选器),未知字段会 warn 日志 |
| `edit_fields` | `(fields: Vec<&str>)` | 可内联编辑的字段(配合 `get_table_edit`) |
| `fields` | `(fields: JsonValue)` | 直接覆盖字段定义(少用) |
| `hide_fields` | `(hide: Vec<&str>)` | 单独设置隐藏字段(少用,通常在 main_table_fields 中设置) |
| `show_fields` | `(show: Vec<&str>)` | 单独设置显示白名单(少用) |
| `total_fields` | `(fields: Vec<&str>)` | 合计字段(前端底部显示合计行) |
| `join_table` | `(main_table, main_field, join_table, join_field)` | 联表查询(LEFT JOIN) |
| `join_fields` | `(table: &str, fields: JsonValue, index: isize)` | 联表字段定义。index >= 0 插入位置,-1 尾部追加 |
| `order_by` | 无此方法 | 排序通过 `params` 中的 `order` 参数传入,或前端 request 携带 |
#### 终端方法(返回 `JsonValue`)
| `get_table()` | ~287 | `{pages, total, data, columns, filter_fields, search_name, total_fields, btn_all, btn_api, btn_ids}` | 标准分页表格 |
| `get_table_select()` | ~136 | `{pages, total, data}` | 下拉选择器(data 为 `[{value, label}]`) |
| `get_tree(pid_field)` | 3 | 同 get_table + isLeaf | 树形表格(按 pid 分层加载) |
| `get_table_menu(label_field)` | 2 | 同 get_table + label_field | 左侧菜单+右侧详情布局 |
| `get_table_select_tree(pid_field, label_field)` | 2 | `{data, label_field, pid_field}` | 树形选择器 |
| `get_table_edit()` | 0(预留) | `{pages, total, data, columns, edit_fields}` | 内联编辑表格 |
#### 内部处理流程
所有终端方法内部按固定顺序调用:
```
db.table(main_table)
→ db_fields() // 处理 hide/show,构建 all_fields_map 缓存,注册 json/location 字段
→ db_search() // 搜索框 → WHERE field1|field2 LIKE '%search%'
→ db_where() // where_and/where_or → DB 条件(table_multiple 用 LIKE 匹配)
→ db_join() // 联表
→ db_total() // COUNT(*) 计算总数和页数
→ db_order() // 排序
→ db.page(page, limit)
→ db.select() // 执行查询
→ db_table() // 批量 FK 解析(table/table_multiple/tree 字段 ID→label)
→ columns() // 构建前端列定义
```
**all_fields_map 缓存**:`db_fields()` 处理 hide/show 后构建 `self.all_fields_map: JsonValue`(字段名→字段定义映射),供后续 6 个方法共享读取。
**批量 FK 解析**(db_table/db_table_tree):
- `resolve_table_field_batch(field)` — 收集所有唯一 ID,单次 `WHERE id IN (...)` 查询
- `resolve_table_multiple_field_batch(field)` — 同上,用于 JSON 数组字段
- `resolve_tree_field(field, tree_data, key)` — 递归解析父链,tree_data 缓存去重
#### 使用模式
**模式 1:标准表格**(最常见,~200+ 文件)
```rust
fn params(&mut self) -> JsonValue {
Tables::params_table(object! {})
}
fn index(&mut self, request: Request) -> ApiResponse {
let mut table_info = self.table()
.main_table_fields(self.module._table_name(), self.module.fields(), vec!["org_org"], vec![])
.search_fields(vec!["code", "name"])
.filter_fields(vec!["status", "created_at"])
.params(request.body.clone())
.get_table();
// 设置按钮(见下方按钮模式)
ApiResponse::success(table_info, "获取成功")
}
```
**模式 2:下拉选择器**(~136 文件)
```rust
fn params(&mut self) -> JsonValue {
Tables::params_table_select(object! {})
}
fn index(&mut self, request: Request) -> ApiResponse {
let table_info = self.table()
.main_select_fields(self.module._table_name(), vec!["name", "code"])
.params(request.body.clone())
.get_table_select();
ApiResponse::success(table_info, "获取成功")
}
```
**模式 3:树形表格**(3 文件:dept, category, area)
```rust
fn params(&mut self) -> JsonValue {
Tables::params_table_tree(object! {})
}
fn index(&mut self, request: Request) -> ApiResponse {
let mut table_info = self.table()
.main_table_fields(self.module._table_name(), self.module.fields(), vec!["org_auth"], vec![])
.search_fields(vec!["name", "code"])
.params(request.body.clone())
.get_tree(self.module._table_name()); // pid_field = 表名(自引用)
// ...
}
```
**模式 4:菜单布局**(2 文件:org_auth, user_auth)
```rust
let mut table_info = self.table()
.main_table_fields(table_name, fields, vec!["addon_module", "user_user", "api_api"], vec![])
.params(request.body.clone())
.search_fields(vec!["name"])
.get_table_menu("name"); // label_field 用于左侧菜单显示
```
**模式 5:树形选择器**(2 文件:area, category)
```rust
fn params(&mut self) -> JsonValue {
Tables::params_table_select_tree(object! {})
}
fn index(&mut self, request: Request) -> ApiResponse {
let table_info = self.table()
.main_select_fields(self.module._table_name(), vec!["name", "code"])
.params(request.body.clone())
.get_table_select_tree(self.module._table_name(), "name");
ApiResponse::success(table_info, "获取成功")
}
```
**模式 6:联表查询**(少量复杂表格)
```rust
let mut table_info = self.table()
.main_table_fields(self.module._table_name(), object!{}, vec![], vec![])
.params(request.body.clone())
.join_fields("goods_goods", goods_fields, -1) // -1 = 尾部追加
.join_fields(self.module._table_name(), fields, -1)
.join_fields("dict_hscode", hscode_fields, -1)
.join_table(self.module._table_name(), "goods_goods", "goods_goods", "id")
.join_table("goods_goods", "hs_code", "dict_hscode", "code")
.get_table();
```
**模式 7:条件分支**(admin/org 不同视图)
```rust
let mut table_info = if is_admin {
self.table()
.main_table_fields(table_name, fields, vec!["org_org"], vec![])
.filter_fields(vec!["dict_country", "state", "org_org", "created_at", "model"])
.search_fields(vec!["code", "name"])
.params(request.body.clone())
.get_table()
} else {
request.body["where_and"].push(array!["org_org", "=", org_org]).unwrap();
self.table()
.main_table_fields(table_name, fields, vec!["org_org"], vec![])
.filter_fields(vec![])
.search_fields(vec!["code", "name"])
.params(request.body.clone())
.get_table()
};
```
#### 按钮模式
按钮在 `get_table()` 返回后手动设置(不是 builder 方法):
```rust
// btn_all: 全局按钮(表格顶部,不依赖选中行)
table_info["btn_all"] = vec![
Plugins::action("addon.module.add").unwrap().btn()
.btn_type(BtnType::Form) // Form=弹窗表单
.btn_color(BtnColor::Primary)
.json(),
Plugins::action("addon.module.import").unwrap().btn()
.btn_type(BtnType::Form)
.btn_color(BtnColor::Green)
.json(),
Plugins::action("addon.module.download_template").unwrap().btn()
.btn_type(BtnType::Download) // Download=文件下载
.btn_color(BtnColor::Primary)
.json(),
].into();
// btn_api: 行按钮(每行操作,可带条件)
table_info["btn_api"] = vec![
Plugins::action("addon.module.put").unwrap().btn()
.btn_type(BtnType::Form)
.btn_color(BtnColor::Primary)
.cnd(vec![array!["status", "=", "草拟中"]]) // 条件显示
.json(),
Plugins::action("addon.module.del").unwrap().btn()
.btn_type(BtnType::Api) // Api=直接调用(确认框)
.btn_color(BtnColor::Red)
.cnd(vec![array!["status", "=", "草拟中"]])
.json(),
Plugins::action("addon.module.detail").unwrap().btn()
.btn_type(BtnType::DialogCustom) // DialogCustom=自定义弹窗
.btn_color(BtnColor::Primary)
.json(),
Plugins::action("addon.module.put_effect").unwrap().btn()
.btn_type(BtnType::Api)
.title("自定义标题") // 覆盖 action title
.btn_color(BtnColor::Green)
.cnd(vec![
array!["status", "in", "待确认,已作废"], // in 操作符
array!["admin", "=", true], // boolean 条件
])
.json(),
].into();
// btn_ids: 批量操作按钮(多选后显示)
table_info["btn_ids"] = vec![
Plugins::action("addon.module.batch_del").unwrap().btn()
.btn_type(BtnType::Api)
.btn_color(BtnColor::Red)
.json(),
].into();
// 权限过滤(必须)
table_info["btn_all"] = check_auth(table_info["btn_all"].clone(), request.clone());
table_info["btn_api"] = check_auth(table_info["btn_api"].clone(), request.clone());
table_info["btn_ids"] = check_auth(table_info["btn_ids"].clone(), request.clone());
```
**BtnType 枚举**:`Form`(弹窗表单)、`Api`(直接调用+确认框)、`DialogCustom`(自定义弹窗页面)、`Download`(文件下载)
**BtnColor 枚举**:`Primary`(蓝)、`Red`(红)、`Green`(绿)、`Yellow`(黄)
**cnd 条件**:只支持 `=`/`<>`/`in` 操作符,不支持数值比较
#### 前端请求参数注入
在调用 `self.table()` 之前,可向 `request.body` 注入额外条件:
```rust
// 注入 where_and 条件(最常见)
request.body["where_and"].push(array!["org_org", "=", org_org]).unwrap();
request.body["where_and"].push(array!["status", "<>", "已删除"]).unwrap();
// 注入 where_or 条件
request.body["where_or"].push(array!["org_org", "=", org_org]).ok();
request.body["where_or"].push(array!["org_org", "=", ""]).ok();
```
#### 注意事项
- `main_table_fields` 或 `main_select_fields` 必须在 `params` 之前调用
- `search_fields` 和 `filter_fields` 可在 `params` 前后调用
- `get_table()` 返回的 `btn_all/btn_api/btn_ids` 默认为空数组,需手动设置
- `get_table_select()` 的 `data` 是 `[{value, label}]` 格式,label 由 `main_select_fields` 指定的字段用 ` | ` 拼接
- `get_tree()` 的 `pid_field` 通常是表名本身(自引用外键,如 `dict_area` 表的 `dict_area` 字段)
- `table_multiple` 字段的 where_and 过滤使用 LIKE 匹配(JSON TEXT 列),不是精确 IN 查询
- 联表字段自动加 `{table}_{field}` 前缀避免冲突
### 全局状态
- `PLUGIN_TOOLS`: `OnceLock<Tools>` 写入一次后只读,无锁访问
- `CONFIG`: `lazy_static! Mutex<HashMap>` 存储配置
- `GLOBAL_DATA`: `thread_local! RefCell<JsonValue>` 线程局部变量
- `GLOBAL_HANDLE`: `LazyLock<Mutex<HashMap>>` 监听线程注册
- `GLOBAL_ADDONS/MODULE/ACTION`: `OnceLock<Vec<String>>` 一次性初始化
### 标注规范
- 构造函数和返回新值的方法加 `#[must_use]`(如 `Swagger::new()`、`Btn::new()`)
- `tables.rs` 整个模块允许 `#[allow(clippy::too_many_arguments)]`
## ANTI-PATTERNS(禁止)
- ❌ 非 feature-gated 代码中直接使用 `self.tools().db`
- ❌ 添加新的全局 `lazy_static!` 变量(新增全局状态用 `LazyLock` 或 `OnceLock`)
- ❌ Action 中直接 `panic!`,应使用 `ApiResponse::fail()`
- ❌ 用 `.unwrap()` 处理用户输入
- ❌ 直接 `.leak()` 泄漏字符串,应使用 `Box::leak()` 并添加注释说明
- ❌ 在 `check()` 验证中遗漏 `require` 检查
- ❌ 使用 `serde_json`,本项目统一用 `json` crate
## GIT COMMIT
格式:`<类型> <描述>`(中文描述)
| A | 新增 | `A fields` |
| U | 更新/优化 | `U 优化addon` |
| F | 修复 | `F 修复参数验证` |
| D | 删除 | `D 删除废弃模板` |
| R | 重构 | `R 重构Tables构建器` |
## KEY PATTERNS
### 实现新插件
参考 `examples/addon/` 目录,需实现三层 trait:
1. `Addon` trait → `fn module(&mut self, name: &str) -> Result<Box<dyn Module>, String>`
2. `Module` trait → `fn action(&mut self, name: &str) -> Result<Box<dyn Action>, String>`
3. `Action` trait → `fn title()` + `fn index(&mut self, request: Request) -> ApiResponse`
### Trait Bounds
- `Addon: Send + Sync + 'static`
- `Module: Send + Sync + 'static`
- `Action` 无额外 bounds(但通过 `Box<dyn Action>` 传递)
实现 struct 通常用 `#[derive(Debug, Clone)]`,内部持有对应 Module struct 引用。
### Trait 必须实现 vs 可选方法
大部分 trait 方法有默认实现,只需覆盖必要的:
| `Addon` | `title()`, `module()` | `icon()`, `sort()`, `description()` | `name()` 自动推导 |
| `Module` | `title()`, `action()` | `fields()`, `table()`, `table_key()`, `table_unique()`, `table_index()` | `_name()`, `_table_name()` 自动推导 |
| `Action` | `title()`, `index()` | `params()`, `method()`, `tags()`, `auth()`, `public()`, `description()` | `api()`, `module_name()` 自动推导 |
### Action struct 惯用写法
每个 Action struct 持有对应 Module struct 引用,这是固定模式:
```rust
#[derive(Debug, Clone)]
pub struct DmsTaskTable {
pub module: DmsTask, // 持有 Module struct
}
impl Action for DmsTaskTable {
fn title(&self) -> &'static str { "表格" }
fn index(&mut self, request: Request) -> ApiResponse {
// 通过 self.module 访问 Module 层方法
// 通过 self.tools() 访问 DB/Cache
}
}
```
Module struct 在 `mod.rs` 中定义,所有同模块 Action 共享同一个 Module struct 类型。
### Action 请求生命周期
`run()` 是外部入口,`index()` 是业务逻辑入口。流程:
```
run(request)
→ 检查 HTTP method 是否匹配 self.method()
→ 如果 params_check() == true:
→ check(&mut request.query, self.query()) // 验证地址参数
→ check(&mut request.body, self.params()) // 验证请求体参数
→ index(request) // 执行业务逻辑
→ 根据 ApiResponse.success 转为 Ok/Err 返回
```
`check()` 会自动移除 params 中未定义的字段,并对缺失字段填充 `field["def"]` 默认值。
### ApiResponse 双态返回
`index()` 返回 `ApiResponse`,`run()` 根据 `success` 字段拆分:
```rust
// index() 内部 — 直接返回 ApiResponse
fn index(&mut self, request: Request) -> ApiResponse {
ApiResponse::success(data, "获取完成") // success=true → run() 返回 Ok(...)
ApiResponse::fail(1000, "参数错误") // success=false → run() 返回 Err(...)
}
```
### br_fields 字段定义
`params()` 中用 `br_fields` 定义请求参数,`fields()` 中定义数据库字段:
```rust
fn params(&mut self) -> JsonValue {
let mut fields = object! {};
// Str::new(require, name, title, max_len, default)
fields["name"] = br_fields::str::Str::new(true, "name", "名称", 50, "").field();
// Int::new(require, name, title, max, default)
fields["age"] = br_fields::int::Int::new(false, "age", "年龄", 200, 0).field();
// Switch::new(require, name, title, default)
fields["active"] = br_fields::int::Switch::new(false, "active", "启用", true).field();
fields
}
```
`field()` 生成 check 验证用的 JSON,`swagger()` 生成 API 文档用的 JSON。
### Tools 访问与 DB 查询链
通过 `self.tools()` 获取 Tools 实例,DB 操作使用链式调用:
```rust
// Module 层快捷方法
self.db_find("id_value"); // 按 id 查单条
self.db_insert(data); // 插入
self.db_update("id_value", data); // 按 id 更新
self.db_delete("id_value"); // 按 id 删除
// Action 层完整链式查询
let mut binding = self.tools();
let db = binding.db.table("table_name");
db.where_and("field", "=", value.into());
db.where_and("status", "in", "a,b,c".into());
let result = db.find(); // 单条
let list = db.select(); // 多条
let count = db.count(); // 计数
let col = db.column("field"); // 单列
```
**注意**:`self.tools()` 每次调用都会 clone 整个 Tools,频繁调用时先绑定到变量。
### 脚手架生成
`addon_create()` 从 `temp/` 模板生成代码,模板按 action 名称匹配:
table/add/del/get/put/select/select_tree/tree/menu/down/import
### 参数验证字段类型
`check()` 支持:key, text, table, tree, file, int, timestamp, yearmonth, float,
string, url, time, code, pass, email, location, color, date, barcode, datetime,
editor, tel, dict, switch, select, radio, array, polygon, object
## NOTES
- `dev-dependencies` 使用本地路径 `../br-web-server`,CI 需特殊处理
- `br-db` 当前使用本地路径 `../br-db`(Cargo.toml 中有注释掉的 crates.io 版本)
- 无 rustfmt.toml / clippy.toml,使用默认配置
- 无 CI 配置文件,依赖手动执行 fmt + clippy