配合 v1-legacy 维护分支的拆分,记录 master 这条线(resx 默认实现、字段级脱敏、 ResFlags 权限开关、虚拟资源、读写钩子编排)目前有哪些能力,方便新接入的项目 了解现状,也跟 v1-legacy 分支做区分。
85 lines
5.8 KiB
Markdown
85 lines
5.8 KiB
Markdown
# git.fsdpf.net/go/req(master)
|
||
|
||
资源系统的核心抽象与默认实现,配合 `framework-v2`/`contracts-v2`/`orm-v2` 等 "-v2" 系列项目使用。
|
||
|
||
## 核心接口(仓库根目录)
|
||
|
||
- `Resource`:一个业务资源的完整描述——字段列表、主键、变更留痕角色、数据库连接、以及
|
||
`GetDBTable(u User, opts ...ResOption) *db.SelectDataset` 这个统一的查询入口。
|
||
- `ResField`:单个字段的描述(code、数据类型、默认值、是否虚拟计算列、`GetRoles()` 权限角色等)。
|
||
- `QueryField`:`ResField` 之上再包一层查询专用信息(别名、是否表达式、omitempty、忽略),
|
||
给 API 层按请求参数动态拼查询列表用。
|
||
- `User`:当前操作者(角色列表、匿名判断等),贯穿权限判断的所有环节。
|
||
- `GetResource`/`MustResource`:按 code/table/uuid 查资源的 DI 注入函数类型,由上层
|
||
(`framework-v2`)注册实现,`resx` 包只依赖类型,不关心具体实现。
|
||
- `ResFlags`(`ResRow`/`ResRowRelations`/`ResMask`/`ResMaskRelations`/`ResAll`):`GetDBTable`
|
||
的 `WithPermission` 选项用的位标志,见下文"权限开关"。
|
||
|
||
## resx 包:默认实现
|
||
|
||
### 构造资源
|
||
|
||
- `resx.New(container do.Injector, code, table string, opts ...Option) req.Resource`:构造一个
|
||
绑定物理表的资源。`container` 是 `samber/do/v2` 的 DI 容器,资源自己的读写钩子
|
||
(`DataInterceptor`/`ResChangeEventFunc`/`MaskFunc`)都是运行时从这个容器里解析的,容器里没
|
||
注册时优雅降级为空操作,所以 `resx` 在测试或不需要这些能力的场景下也能独立使用。可选项:
|
||
`WithUuid`/`WithName`/`WithDescription`/`WithConn`/`WithPrimarykey`/`WithHistoryRoles`/
|
||
`WithFields`。
|
||
- `resx.NewVirtualResource(parent req.Resource, code string, table exp.SQLExpression, opts ...Option) req.Resource`:
|
||
基于一个已有资源(复用它的 `container`/`conn`/`historyRoles`)和一段查询表达式(子查询/窗口
|
||
函数等)构造只读虚拟资源,`table` 不是物理表名,而是 `GetTableExpr()` 直接拿来拼 FROM 子句的
|
||
表达式。虚拟资源会跳过行级权限过滤和写操作相关的钩子编排(本来就是只读的)。
|
||
- `resx.NewResField(code, codeResource string, opts ...ResFieldOption)`:构造字段,`ResField`
|
||
内部字段全私有,只能通过 `FieldWith*`(`FieldWithName`/`FieldWithDataType`/`FieldWithRoles`/
|
||
`FieldWithVirtual` 等)设置。
|
||
- `resx.NewQueryField(rField req.ResField, t req.RouteParamType, opts ...QueryFieldOption)`:
|
||
`QueryField` 嵌入的是 `req.ResField` **接口**而不是具体结构体,调用方手上已有的
|
||
`req.ResField` 可以直接传入,不用现造一个。
|
||
|
||
### 字段级权限脱敏
|
||
|
||
- `resx.MaskField("table.col")` / `resx.MaskField("col")`:在 SELECT 列表里显式标记一列需要按
|
||
权限脱敏。**只有标记过的列才会被处理**,未标记的列即使字段本身配置了 `Roles` 也不会被动过
|
||
——标记是显式的,`dataProcessor` 不会替调用方去猜哪一列对应哪个字段。
|
||
- 标记的列在查询真正执行前(`exec.Hooks.Before`,这时候完整的 FROM/JOIN 子句才拼好)才懒解析:
|
||
裸列名或者别名就是当前资源自己时直接用当前资源,不碰 DI;别名对应 JOIN 进来的另一个资源时,
|
||
先在 FROM/JOIN 子句里把别名换成真实表名,再用 `req.GetResource` 按真实表名查到对应资源。
|
||
按角色(`ResField.GetRoles()`)判断当前用户有没有权限看到真实值,没有的话换成哨兵值——数字类
|
||
默认 `resx.DefaultMaskInt`(`-999999999`),字符串类默认 `resx.DefaultMaskString`(三个不可见
|
||
字符拼的短横线,跟真实短横线区分得开),也可以在容器里注册 `resx.MaskFunc` 自定义脱敏值。
|
||
- `SELECT *`(或者显式选中当前资源自己别名的全部列)会先判断有没有任何字段配置了 `Roles`,有
|
||
的话才展开成显式字段列表逐个按权限脱敏,没有的话原样保留 `SELECT *`,避免不必要的改写。
|
||
- 写操作(INSERT/UPDATE)里,没有写权限的字段会被 `normalizeSaveValue` 静默丢弃,不会出现在
|
||
最终的 SQL 里。
|
||
|
||
### 权限开关(`req.WithPermission`)
|
||
|
||
`GetDBTable(u, opts...)` 默认(不传 `WithPermission`)**行级过滤和字段脱敏都跳过**——这是零值
|
||
语义,两个开关相互独立:
|
||
|
||
```go
|
||
res.GetDBTable(u, req.WithPermission(req.ResRow)) // 只开行级过滤
|
||
res.GetDBTable(u, req.WithPermission(req.ResMask)) // 只开字段脱敏
|
||
res.GetDBTable(u, req.WithPermission(req.ResAll)) // 两个都开(ResRow|ResMask)
|
||
```
|
||
|
||
### 读写钩子编排
|
||
|
||
三个函数类型,由上层用 `do.Provide` 注册到容器,`resx` 只依赖类型本身:
|
||
|
||
- `resx.DataInterceptor`:每次读写前调用,返回行级权限过滤条件(`sub` 子查询或 `cond`
|
||
表达式,二选一)、以及这次写操作要不要在写完后回调(`onChange`,只对 INSERT/UPDATE/DELETE
|
||
有意义,SELECT 恒为 nil)。
|
||
- `resx.ResChangeEventFunc`:写操作完成后无条件调用一次(典型用途:发布"资源变了"的粗粒度事件,
|
||
比如清缓存)。
|
||
- `resx.ResChangeRowFunc`:由 `DataInterceptor` 返回的 `onChange` 提供,需要抓取变更快照时才
|
||
调用(典型用途:写变更日志、发布 ResWatcher 事件)。`onChange` 非 nil 时写操作会自动包一层
|
||
事务,保证写入和 `onChange` 本身是同一个事务。
|
||
|
||
容器里没注册对应钩子时,三个都优雅降级为空操作,不影响基本读写。
|
||
|
||
## 分支状态
|
||
|
||
持续开发中,**不保证跟旧 API 兼容**。老项目(还没迁移到上面这些新能力的)不要直接跟着这个分支
|
||
走,参见 `v1-legacy` 分支。
|