新增 resx 包剩余实现:字段/查询字段/脱敏/变更行等类型及测试

res_field.go/res_field_option.go(字段定义)、res_query_field.go/res_query_field_option.go(查询字段)、res_change_row.go(变更行)、res_mask_field.go(字段脱敏标记)及对应测试文件,配合此前已提交的 resource.go/res_interceptor.go 组成完整的 resx 资源实现包。
This commit is contained in:
2026-07-22 09:11:50 +08:00
parent 1868c8dd15
commit 09f4c40d67
10 changed files with 2427 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
package resx
import (
"fmt"
"git.fsdpf.net/go/db/exp"
"git.fsdpf.net/go/req"
)
// MaskField 用 "table.col" 或裸列名 "col" 标记一个列需要按对应 req.ResField 的 Roles 做字段级
// 权限脱敏。dataProcessor.maskSelect 只处理带这个标记的列,未标记的列(包括看起来像同名字段的
// 普通列)一律不处理——标记是显式的,不靠猜。
//
// 不带表名的裸列名(比如 "salary")表示当前资源自己的字段;带表名的(比如 "Department.salary"
// 里的 table 部分通常是查询里用的别名(当前资源自己的默认别名,或者 JOIN 进来的资源用的别名)。
// 这里不会立即去查是哪个资源、哪个字段:MaskField 被调用的时候,SELECT 列表所在的 SelectDataset
// 可能还没拼完(FROM/JOIN 子句可能还没加上去),而且别名也不一定等于资源自己的 code(比如同一个
// 资源自连接查询,两个别名不可能都等于资源自己的 code)——这两种情况都得等查询真正执行前
// dataProcessor.maskSelect,在 exec.Hooks.Before 里跑,那时候才有完整的 FROM/JOIN 子句)才能
// 正确解析出别名对应的真实表名,再去 container 里的 req.GetResource 按 code/table/uuid 查资源、
// 按 col 查字段。
//
// 用法:resx.MaskField("salary").As("salary")、resx.MaskField("Department.salary").As("salary")
func MaskField(ident string) exp.IdentifierExpression {
parsed := exp.ParseIdentifier(ident)
code, isStringCode := parsed.GetCol().(string)
if !isStringCode || code == "" {
panic(fmt.Errorf("resx.MaskField: ident %q 不是合法的列名", ident))
}
return lazyMaskedIdent{IdentifierExpression: parsed, codeResource: parsed.GetTable(), code: code}
}
// lazyMaskResolver 是 lazyMaskedIdent/lazyMaskedAliased 共用的类型断言接口:dataProcessor.maskSelect
// 靠它从 SELECT 列表里挑出被 MaskField 标记过、还没解析成具体 req.ResField 的列。codeResource 为
// 空表示裸列名,对应当前资源自己的字段。
//
// 这里特意不叫 aliasMaskField("table.col") 里 table 这部分,语义上对应的是
// req.ResField.GetCodeResource()(哪个资源),不是 SQL 里"起别名"的 alias(比如 .As(...) 里的
// 输出列名)——这个包里两种"alias"都会出现(resolveJoinTable 是在拿它去 FROM/JOIN 子句里当 SQL
// 别名搜,那里用 alias 这个词没问题;但站在 MaskField 被构造出来这一刻,它就是资源标识,叫
// codeResource 才不会跟 .As(...) 那个"别名"混在一起)。
type lazyMaskResolver interface {
lazyMaskField() (codeResource, code string)
}
// lazyMaskedIdent 包装一个 exp.IdentifierExpression,同时打上"待解析"标记(只有资源标识+字段
// code,还没查出 req.ResField)。除 As() 外的其它方法(Eq()/In()/Table() 等)都是内嵌接口自动
// 透传,不用手写。
type lazyMaskedIdent struct {
exp.IdentifierExpression
codeResource string
code string
}
func (m lazyMaskedIdent) lazyMaskField() (string, string) { return m.codeResource, m.code }
// As 必须单独实现:IdentifierExpression.As() 返回的是 exp.AliasedExpression,是另一个类型,
// 如果不单独处理,直接透传内嵌值的 As() 会丢掉标记(调用方几乎总会紧跟着调用 As() 给列起别名)。
func (m lazyMaskedIdent) As(val interface{}) exp.AliasedExpression {
return lazyMaskedAliased{AliasedExpression: m.IdentifierExpression.As(val), codeResource: m.codeResource, code: m.code}
}
// lazyMaskedAliased 包装一个 exp.AliasedExpressionMaskField(...).As(...) 链式调用之后的结果),
// 同时保留"待解析"标记
type lazyMaskedAliased struct {
exp.AliasedExpression
codeResource string
code string
}
func (m lazyMaskedAliased) lazyMaskField() (string, string) { return m.codeResource, m.code }
// hasFieldAccess 判断 u 是否有权限读写 field 的真实值:field.GetRoles() 为空表示不限制,非空时
// u 的角色只要有一个在列表里就算有权限(复用 req.User.HasUserRoles,同时拿到超级管理员的豁免)
func hasFieldAccess(field req.ResField, u req.User) bool {
roles := field.GetRoles()
if len(roles) == 0 {
return true
}
return u.HasUserRoles(roles...)
}