# 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` 分支。