Files
req/README.md
T
what 632c5476a0 docs: 新增 README,说明当前分支的能力范围
配合 v1-legacy 维护分支的拆分,记录 master 这条线(resx 默认实现、字段级脱敏、
ResFlags 权限开关、虚拟资源、读写钩子编排)目前有哪些能力,方便新接入的项目
了解现状,也跟 v1-legacy 分支做区分。
2026-08-20 16:23:41 +08:00

85 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# git.fsdpf.net/go/reqmaster
资源系统的核心抽象与默认实现,配合 `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` 分支。