Files
req/resx/res_interceptor.go
what acb55bb53a 重构: ResScope 改成 ResFlags 位标志,行级过滤与字段脱敏拆成独立开关
ResScope(Off/On/All 三选一)改成 ResFlags 位标志:ResRow/ResRowRelations 管行级权限过滤,ResMask/ResMaskRelations 管字段级脱敏,两个维度可以按位独立组合(*Relations 那两位只保留定义,还没接入判断逻辑)。WithRolesScope 改名 WithPermission(req.ResFlags),dataProcessor 里原来单一的 skipPermission 拆成 skipRowFilter/skipFieldMask 分别控制。
2026-07-22 14:03:33 +08:00

930 lines
31 KiB
Go
Raw Permalink 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.
package resx
import (
"database/sql"
"fmt"
"log"
"reflect"
"strings"
"github.com/samber/do/v2"
"github.com/samber/lo"
"github.com/spf13/cast"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/exec"
"git.fsdpf.net/go/db/exp"
"git.fsdpf.net/go/req"
)
// changeRow 保存一行数据的主键、UPDATE/DELETE 前的快照,以及 INSERT/UPDATE 时写入的字段值
type changeRow struct {
id int64
snapshot map[string]any
changes map[string]any
}
// dataProcessor 是挂在 db.SelectDataset/InsertDataset/UpdateDataset/DeleteDataset 上的读写钩子。
//
// 职责:
// 1. 权限过滤:根据 DataInterceptor 返回的角色条件改写 SELECT/UPDATE/DELETE 的 WHERE。
// 2. 数据规范化:INSERT/UPDATE 前统一清理系统保留字段、按字段类型转换保存值,并为 INSERT 填充默认值。
// 3. 变更留痕:写操作完成后无条件调用 ResChangeEventFunc;当 DataInterceptor 判断需要时,写操作前
// 抓取快照/写入值,写操作后额外调用 ResChangeRowFunc(由上层实现变更日志写入、ResWatcher
// 事件发布等)。
// 4. 事务托底:当 onChange 会被调用、且调用方没有显式传入事务时,自动开一个事务包住这次写操作和
// onChange 本身;onChange 失败就整体回滚,成功才提交。调用方显式传入的事务不受影响,提交/
// 回滚仍由调用方自己负责。
type dataProcessor struct {
res *resource
u req.User
tx *db.TxDatabase
ownsTx bool // tx 是否由本次写操作自动开启(而非调用方传入),决定 After 要不要 Commit/Rollback
onChange ResChangeRowFunc // 非 nil 表示本次写操作需要抓取快照/收集变更内容,由 Before 阶段的 DataInterceptor 决定
rows []changeRow
silent bool // ResOptions.Silent:跳过变更通知,见 req.WithSilent 注释
skipRowFilter bool // ResOptions.Permission 不含 ResRow:跳过行级权限过滤,见 req.WithPermission 注释
skipFieldMask bool // ResOptions.Permission 不含 ResMask:跳过字段级脱敏/写权限,见 req.WithPermission 注释
}
// UseTx 在 Before 之前调用:如果本次写操作会触发 onChange、且调用方没有显式传入事务,自动开一个
// 事务包住写操作和 onChange。调用方已显式传入的事务(dp.tx != nil)不受影响。
func (dp *dataProcessor) UseTx(dataset interface{}) (exec.QueryFactory, error) {
if dp.res.IsVirtual() || dp.tx != nil {
return nil, nil
}
var category ResEventType
switch dataset.(type) {
case *db.InsertDataset:
category = ResEventInsert
case *db.UpdateDataset:
category = ResEventUpdate
case *db.DeleteDataset:
category = ResEventDelete
default:
return nil, nil
}
dp.applyIntercept(category)
if dp.onChange == nil {
return nil, nil
}
tx, err := dp.res.DB().Begin()
if err != nil {
return nil, err
}
dp.tx = tx
dp.ownsTx = true
return exec.NewQueryFactory(tx), nil
}
// applyIntercept 调用容器里注册的 DataInterceptor,返回角色权限过滤条件,并记录本次写完后要不要调用、调用谁。
// skipRowFilter 时丢弃行级权限过滤条件;silent 时不设置 onChange(写完后不会触发变更通知)。两者
// 相互独立,可以只生效一个。
func (dp *dataProcessor) applyIntercept(category ResEventType) (sub *db.SelectDataset, cond db.Expression) {
sub, cond, onChange := dp.res.intercept(dp.u, category)
if dp.skipRowFilter {
sub, cond = nil, nil
}
if !dp.silent {
dp.onChange = onChange
}
return sub, cond
}
// captureSnapshot 在写操作执行前,查出受影响的行存入 rows
func (dp *dataProcessor) captureSnapshot(where exp.ExpressionList) {
sd := dp.res.GetDBTable(dp.u)
if where != nil && len(where.Expressions()) > 0 {
sd = sd.Where(where.Expressions()...)
}
snaps, err := sd.Executor().GetRecords()
if err != nil {
log.Printf("captureSnapshot err: %v", err)
log.Println(sd.Executor().ToSQL())
return
}
dp.rows = make([]changeRow, len(snaps))
for i, snap := range snaps {
dp.rows[i].snapshot = snap
if id, ok := snap["id"]; ok {
dp.rows[i].id = cast.ToInt64(id)
}
}
}
// insertRowIds 计算本次 INSERT 各行的自增 idMySQL: lastId 是第一条,SQLite: lastId 是最后一条)
func (dp *dataProcessor) insertRowIds(result sql.Result) []int64 {
lastId, _ := result.LastInsertId()
count, _ := result.RowsAffected()
ids := make([]int64, count)
for i := int64(0); i < count; i++ {
rowId := lastId + i
if isLocalDB(dp.res.DB().Dialect()) {
rowId = lastId - count + 1 + i
}
ids[i] = rowId
}
return ids
}
// finalizeInsertedRows 把计算好的自增 id 写回每一行;INSERT ... SELECT 场景 Before 阶段拿不到具体数据,
// 这里改为按 id 反查补全(见 fetchInsertedRows
func (dp *dataProcessor) finalizeInsertedRows(result sql.Result) {
ids := dp.insertRowIds(result)
if len(dp.rows) == 0 {
dp.fetchInsertedRows(ids)
return
}
for i := range dp.rows {
if i < len(ids) {
dp.rows[i].id = ids[i]
}
}
}
// fetchInsertedRows 用于 INSERT ... SELECT 场景:写入前拿不到具体行数据,写入后按 id 反查补全 changes
func (dp *dataProcessor) fetchInsertedRows(ids []int64) {
recs, err := dp.res.GetDBTable(dp.u).Where(db.C("id").In(ids)).Executor().GetRecords()
if err != nil {
log.Printf("fetchInsertedRows err: %v", err)
return
}
byId := make(map[int64]map[string]any, len(recs))
for _, rec := range recs {
if id, ok := rec["id"]; ok {
byId[cast.ToInt64(id)] = rec
}
}
dp.rows = make([]changeRow, len(ids))
for i, id := range ids {
dp.rows[i].id = id
if rec, ok := byId[id]; ok {
dp.rows[i].changes = dp.changesFromNewRow(rec)
}
}
}
// buildChanges 将本次 UPDATE 实际写入的字段值记入每一行的 changes
func (dp *dataProcessor) buildChanges(newValues db.Record) {
if len(dp.rows) == 0 || len(newValues) == 0 {
return
}
resolved := make(map[string]any, len(newValues))
for k, v := range newValues {
resolved[k] = dp.resolveLiteralValue(v)
}
for i := range dp.rows {
dp.rows[i].changes = resolved
}
}
// resolveLiteralValue 从 db.V/db.L 构造的字面量表达式中还原真实值,用于变更对比与序列化
func (dp *dataProcessor) resolveLiteralValue(v any) any {
lit, ok := v.(exp.LiteralExpression)
if !ok {
return v
}
if lit.Literal() == "?" {
if args := lit.Args(); len(args) == 1 {
return args[0]
}
}
return lit.Literal()
}
// changesFromNewRow 还原 INSERT 行的最终保存值,作为 changes 直接记录
func (dp *dataProcessor) changesFromNewRow(row map[string]any) map[string]any {
data := make(map[string]any, len(row))
for k, v := range row {
data[k] = dp.resolveLiteralValue(v)
}
return data
}
func (dp *dataProcessor) Before(dataset interface{}) *db.HookError {
if err := dp.before(dataset); err != nil {
return &db.HookError{Err: err}
}
return nil
}
func (dp *dataProcessor) before(dataset interface{}) error {
switch d := dataset.(type) {
case *db.SelectDataset:
return dp.beforeSelectDataset(d)
case *db.InsertDataset:
return dp.beforeInsertDataset(d)
case *db.UpdateDataset:
return dp.beforeUpdateDataset(d)
case *db.DeleteDataset:
return dp.beforeDeleteDataset(d)
}
return nil
}
func (dp *dataProcessor) After(dataset interface{}, result interface{}) *db.HookError {
if err := dp.after(dataset, result); err != nil {
return &db.HookError{Err: err}
}
return nil
}
func (dp *dataProcessor) after(dataset interface{}, result interface{}) error {
if dp.res.IsVirtual() {
return nil
}
var category ResEventType
switch dataset.(type) {
case *db.SelectDataset:
return nil
case *db.InsertDataset:
category = ResEventInsert
case *db.UpdateDataset:
category = ResEventUpdate
case *db.DeleteDataset:
category = ResEventDelete
default:
return nil
}
r := result.(sql.Result)
if !dp.silent {
if err := dp.res.changeEvent(NewResChangeEvent(category, dp.res, dp.u, r)); err != nil {
log.Printf("ResChangeEventFunc err: %v", err)
}
}
// ResChangeRowFunc 由 Before 阶段 DataInterceptor 返回的 onChange 直接提供,非 nil 才调用
var hookErr error
if dp.onChange != nil {
if category == ResEventInsert {
dp.finalizeInsertedRows(r)
}
rows := make([]ResChangeRow, len(dp.rows))
for i, row := range dp.rows {
rows[i] = NewResChangeRow(category, dp.res, dp.u, row.id, WithOld(row.snapshot), WithNew(row.changes), WithTx(dp.tx))
}
hookErr = dp.onChange(rows)
}
if dp.ownsTx {
if hookErr != nil {
if rbErr := dp.tx.Rollback(); rbErr != nil {
log.Printf("dataProcessor tx rollback err: %v", rbErr)
}
} else if cmErr := dp.tx.Commit(); cmErr != nil {
hookErr = cmErr
}
}
return hookErr
}
func (dp *dataProcessor) beforeInsertDataset(id *db.InsertDataset) error {
// INSERT 不涉及权限过滤(新建的行无需按角色可见性过滤),只关心是否需要抓取变更内容
dp.applyIntercept(ResEventInsert)
switch true {
case id.GetClauses().HasRows():
return dp.beforeInsertRows(id)
case id.GetClauses().HasVals():
return dp.beforeInsertColsVals(id)
case id.GetClauses().HasFrom():
return dp.beforeInsertFromQuery(id)
}
return nil
}
func (dp *dataProcessor) beforeInsertRows(id *db.InsertDataset) error {
rows := id.GetClauses().Rows()
for i := range rows {
rowValue := reflect.ValueOf(rows[i])
if rowValue.Kind() == reflect.Ptr {
rowValue = rowValue.Elem()
}
if rowValue.Kind() == reflect.Struct {
if row, err := exp.NewRecordFromStruct(rowValue.Interface(), true, false); err != nil {
return err
} else {
rows[i] = row
}
}
}
for i := range rows {
row, ok := rows[i].(map[string]any)
if !ok {
return fmt.Errorf("row is %T, want map[string]any", row)
}
if err := dp.normalizeSaveValue(row); err != nil {
return err
}
if err := dp.applyDefaultValue(row, true, false); err != nil {
return err
}
if dp.onChange != nil {
dp.rows = append(dp.rows, changeRow{changes: dp.changesFromNewRow(row)})
}
}
return nil
}
func (dp *dataProcessor) beforeInsertColsVals(id *db.InsertDataset) error {
cols := id.GetClauses().Cols()
vals := id.GetClauses().Vals()
colsName := []string{}
for _, col := range cols.Columns() {
colsName = append(colsName, col.(exp.IdentifierExpression).GetCol().(string))
}
extraColsName := []string{}
colsVal := []any{}
for k, v := range lo.OmitByKeys(dp.getFieldsDefaultValue(true, false), colsName) {
cols = cols.Append(db.C(k))
extraColsName = append(extraColsName, k)
colsVal = append(colsVal, v)
}
allColsName := append(append([]string{}, colsName...), extraColsName...)
for i := 0; i < len(vals); i++ {
for j := 0; j < len(vals[i]); j++ {
if filed, ok := dp.res.GetField(colsName[j]); ok {
vals[i][j] = filed.ToValue(vals[i][j])
}
}
vals[i] = append(vals[i], colsVal...)
if dp.onChange != nil {
row := make(map[string]any, len(allColsName))
for j, name := range allColsName {
if j < len(vals[i]) {
row[name] = vals[i][j]
}
}
dp.rows = append(dp.rows, changeRow{changes: dp.changesFromNewRow(row)})
}
}
*id = *id.Cols(cols)
return nil
}
// beforeInsertFromQuery INSERT ... SELECT:写入内容来自子查询,写入前无法拿到具体行数据,
// changes 由 After 在写入完成后按 id 反查补全(见 fetchInsertedRows
func (dp *dataProcessor) beforeInsertFromQuery(id *db.InsertDataset) error {
return nil
}
// normalizeSaveValue 规范化保存数据:
// 1. 移除系统缺省字段, 如果是 insert 需要调用 applyDefaultValue
// 2. 移除系统中没有的字段
// 3. 移除当前用户没有权限写的字段(ResField.GetRoles,语义同查询脱敏,见 hasFieldAccess)——
// 静默丢弃而不是报错,跟"系统中没有的字段"一致:数据库里对应列保持原值不变。skipPermission
// 时跳过这条检查,字段规范化/类型转换本身不受影响。
func (dp *dataProcessor) normalizeSaveValue(row db.Record) error {
for k, v := range row {
if k == "id" || k == "created_user" || k == "created_at" || k == "deleted_at" || k == "updated_at" {
delete(row, k)
continue
}
field, hasField := dp.res.GetField(k)
if hasField && !dp.skipFieldMask && !hasFieldAccess(field, dp.u) {
delete(row, k)
continue
}
if val, ok := v.(db.Expression); ok {
row[k] = val
} else if hasField {
row[k] = field.ToValue(v)
} else {
delete(row, k)
}
}
return nil
}
// applyDefaultValue 填充默认数据
func (dp *dataProcessor) applyDefaultValue(row db.Record, forInsert, forUpdate bool) error {
for k, v := range dp.getFieldsDefaultValue(forInsert, forUpdate) {
if _, ok := row[k]; !ok {
row[k] = v
}
}
return nil
}
func (dp *dataProcessor) getFieldsDefaultValue(forInsert, forUpdate bool) map[string]db.Expression {
defs := map[string]db.Expression{}
if forInsert {
for _, item := range dp.res.fields {
if item.GetCode() == "updated_at" || item.GetCode() == "created_user" || item.GetCode() == "owned_user" {
continue
}
raw := item.GetRawDefault()
if raw != nil {
if lit, ok := raw.(exp.LiteralExpression); ok && strings.HasPrefix(strings.ToLower(lit.Literal()), "sql:") {
defs[item.GetCode()] = raw
} else if item.GetDataType() == req.ResJson {
defs[item.GetCode()] = raw
}
}
}
if _, ok := defs["owned_user"]; !ok {
defs["owned_user"] = db.V(dp.u.Uuid())
}
defs["created_user"] = db.V(dp.u.Uuid())
}
if forUpdate && isLocalDB(dp.res.DB().Dialect()) {
defs["updated_at"] = db.L("CURRENT_TIMESTAMP")
}
return defs
}
func (dp *dataProcessor) beforeSelectDataset(sd *db.SelectDataset) error {
sub, ex := dp.applyIntercept(ResEventSelect)
if ex != nil {
*sd = *sd.Where(ex)
} else if sub != nil {
table := sd.GetClauses().From().Columns()[0]
if alias, ok := table.(exp.AliasedExpression); ok {
*sd = *sd.From(db.V(sub.Expression()).As(alias.GetAs()))
} else {
*sd = *sd.From(db.V(sub.Expression()).As(table))
}
}
return dp.maskSelect(sd)
}
// maskSelect 按字段级权限(ResField.GetRoles)替换 SELECT 列表里当前用户没有权限查看的字段:
// - 没有显式选择过列(裸的 db.Star()),或者显式选了限定到当前资源自己别名的 db.T(alias).All()
// ——两种写法效果一样,都是"选当前资源自己的全部列":展开成资源自己的全部字段,逐个按权限
// 判断要不要替换成脱敏值。只覆盖当前资源自己的字段,JOIN 进来的字段没有被展开进来,覆盖不到。
// - 其它情况下显式选择了列(通常来自 orm-v2 QueryField.ToSql()):只处理其中被 MaskField(...)
// 显式标记过的列,未标记的列一律原样保留——标记是显式的,不去猜哪一列对应哪个字段。这时候才
// 真正解析 MaskField 标记的别名对应哪个资源、哪个字段(见 resolveLazyMask),解析失败说明
// 调用方标记的表名/字段名写错了,是编程错误,直接报错而不是静默跳过。
func (dp *dataProcessor) maskSelect(sd *db.SelectDataset) error {
if dp.res.IsVirtual() || dp.skipFieldMask {
return nil
}
if !dp.hasRestrictedFields(sd) {
return nil
}
if dp.isDefaultSelect(sd) {
if len(dp.res.fields) == 0 {
return nil
}
cols := make([]interface{}, len(dp.res.fields))
changed := false
for i, field := range dp.res.fields {
if hasFieldAccess(field, dp.u) {
cols[i] = db.C(field.GetCode())
} else {
cols[i] = dp.res.mask(field).As(field.GetCode())
changed = true
}
}
if changed {
*sd = *sd.Select(cols...)
}
return nil
}
old := sd.GetClauses().Select().Columns()
cols := make([]interface{}, len(old))
changed := false
for i, c := range old {
resolved, colChanged, err := dp.resolveMaskedExpr(sd, c, true)
if err != nil {
return err
}
cols[i] = resolved
if colChanged {
changed = true
}
}
if changed {
*sd = *sd.Select(cols...)
}
return nil
}
// hasRestrictedFields 判断这次查询可能碰到的字段——当前资源自己的,加上 FROM/JOIN 子句里所有
// 能解析到具体资源的——里有没有任何一个配置了 Roles。一个都没有的话,maskSelect 后面所有的工作
// SELECT * 展开、显式 MaskField 列解析)都不会有任何效果,可以在最前面直接跳过整个函数,不用
// 再管是不是默认 SELECT、SELECT 里有没有标记过的列。
//
// JOIN 的表如果解析不到具体资源(不是这套资源系统管理的表、容器没注册 req.GetResource、或者
// 这张表在 FROM/JOIN 里的写法认不出真实表名),不能当成"没有受限字段"直接放过——万一这张表上
// 真的有 MaskField(...) 标记,那本该在 resolveLazyField 里报错(表名/字段名配置错误),提前跳过
// 就变成静默放过,比现在的报错行为更危险。这些拿不准的情况一律返回 true(不跳过),退回到老路径
// 让后面该报错报错、该处理处理。
//
// 有个前提:这里只看 sd 真实的 FROM/JOIN 子句。MaskField("alias.col") 里 alias 指向一个压根没
// JOIN 进这次查询的资源时(resolveLazyField 会把 alias 当 code/table/uuid 兜底直接查一次),这
// 里扫不到那个资源,如果它自己没有受限字段又恰好没被扫进来,会被误判成"整个查询不用脱敏"直接跳过
// ——这种查询本来就没有实际意义(列不属于 FROM 里的任何一张表,真跑数据库会报错),MaskField 的
// alias 必须对应查询里真实 JOIN 过的表,是这个快速跳过路径的前提。
//
// 超级管理员(HasUserRoles() 不带参数调用,只命中超管角色那一支)对任何字段都有权限,不管查了
// 哪些表、字段配置了什么 Roles,脱敏最终都不会生效(hasFieldAccess 对超管永远返回 true)——直接
// 跳过整个解析过程,既是性能优化,也避免了系统自己在资源初始化阶段用系统用户递归发起查询时,
// req.GetResource 还没注册好导致的 DI 循环依赖报错(跟 framework-v2 的 res_scope.go 里
// getResUserRolesCondition 对同类问题的处理是一个思路)。
func (dp *dataProcessor) hasRestrictedFields(sd *db.SelectDataset) bool {
if dp.u.HasUserRoles() {
return false
}
if dp.fieldsHaveRoles(dp.res.fields) {
return true
}
getResource, err := do.Invoke[req.GetResource](dp.res.container)
if err != nil {
return true
}
clauses := sd.GetClauses()
tables := make([]exp.Expression, 0, 1+len(clauses.Joins()))
if from := clauses.From().Columns(); len(from) > 0 {
tables = append(tables, from[0])
}
for _, j := range clauses.Joins() {
tables = append(tables, j.Table())
}
for _, t := range tables {
aliased, ok := t.(exp.AliasedExpression)
if !ok {
return true
}
table, ok := dp.realTableOf(aliased.Aliased())
if !ok {
return true
}
res, ok := getResource(table)
if !ok {
return true
}
if dp.fieldsHaveRoles(res.GetFields()) {
return true
}
}
return false
}
// fieldsHaveRoles 判断 fields 里有没有任何一个配置了 RoleshasRestrictedFields 对当前资源自己
// 和每个能解析到的 JOIN 资源都要做一次这个判断)
func (dp *dataProcessor) fieldsHaveRoles(fields []req.ResField) bool {
for _, field := range fields {
if len(field.GetRoles()) > 0 {
return true
}
}
return false
}
// resolveMaskedExpr 递归处理 c 内部被 MaskField 标记的列,包括嵌套在 db.Func(...) 参数里的情况
// (比如 db.Func("CONCAT", resx.MaskField("t.a"), resx.MaskField("t.b")))——顶层 SELECT 列表
// 只按类型断言直接标记的列,标记被函数包一层之后顶层类型就变成了 SQLFunctionExpression,断言不
// 出来,得递归进 Args() 里找。
//
// topLevel 为 true 表示 c 本身就是一整个 SELECT 列,这时候没权限要换成 "mask值 AS 别名"
// topLevel 为 false 表示 c 是嵌在别的表达式(目前只处理 db.Func 的参数)里面,这时候只能换成
// 裸的 mask 值,不能带 AS——SQL 函数参数没法单独起别名。
//
// 目前只递归处理 SQLFunctionExpressiondb.Func(...))和包着它的 AliasedExpression
// db.Func(...).As(...)):这是这个代码库里唯一会把 MaskField 包起来用的场景(聚合/计算字段),
// 没有覆盖 CastExpression/CaseExpression/WindowExpression 等目前用不到的包装类型。
func (dp *dataProcessor) resolveMaskedExpr(sd *db.SelectDataset, c interface{}, topLevel bool) (interface{}, bool, error) {
if r, ok := c.(lazyMaskResolver); ok {
field, unresolved, err := dp.resolveLazyField(sd, r)
if err != nil {
return nil, false, err
}
if unresolved {
// req.GetResource 本身解析失败(见 resolveLazyField 的说明),没有具体字段可参考
// 数据类型,退到 db.V(nil) 兜底——跟 defaultMask 认不出数据类型时的兜底分支一致。
if !topLevel {
return db.V(nil), true, nil
}
return db.V(nil).As(dp.fallbackAlias(c, r)), true, nil
}
if hasFieldAccess(field, dp.u) {
return c, false, nil
}
if !topLevel {
return dp.res.mask(field), true, nil
}
return dp.res.mask(field).As(dp.fallbackAlias(c, r)), true, nil
}
if fn, ok := c.(exp.SQLFunctionExpression); ok {
args := fn.Args()
newArgs := make([]interface{}, len(args))
changed := false
for i, a := range args {
resolved, argChanged, err := dp.resolveMaskedExpr(sd, a, false)
if err != nil {
return nil, false, err
}
if argChanged {
changed = true
newArgs[i] = resolved
} else {
newArgs[i] = a
}
}
if !changed {
return c, false, nil
}
return db.Func(fn.Name(), newArgs...), true, nil
}
if aliased, ok := c.(exp.AliasedExpression); ok {
inner, innerChanged, err := dp.resolveMaskedExpr(sd, aliased.Aliased(), false)
if err != nil {
return nil, false, err
}
if !innerChanged {
return c, false, nil
}
return inner.(exp.Aliaseable).As(aliased.GetAs()), true, nil
}
return c, false, nil
}
// fallbackAlias 在拿不到真正的 req.ResField(没法用 field.GetCode())时,给替换后的表达式起个
// 别名:优先用调用方自己指定的别名(MaskField(...).As("xxx")),没有的话退到列名本身
// lazyMaskField() 的第二个返回值)。
func (dp *dataProcessor) fallbackAlias(c interface{}, r lazyMaskResolver) string {
if aliased, ok := c.(exp.AliasedExpression); ok {
if col, ok := aliased.GetAs().GetCol().(string); ok && col != "" {
return col
}
}
_, code := r.lazyMaskField()
return code
}
// resolveLazyField 把 MaskField(...) 标记的"待解析"列解析成真正的 req.ResFieldcodeResource
// 为空表示当前资源自己的字段,直接用 dp.res;否则把 codeResource 当 SQL 别名,在 sd 的 FROM/JOIN
// 子句里换成真实表名(见 resolveJoinTable),换不到就把它本身当 code/table/uuid 兜底试一次,再用
// req.GetResource 查资源。
// unresolved=true 表示 req.GetResource 本身解析失败——常见原因是资源系统自己还在初始化过程中
// 递归触发了一次带 JOIN 的查询,形成真正的循环依赖(不是调用方标记写错了表名/字段名,那种情况
// 会走下面别的报错分支)。这种情况不能让整个查询因为一次 DI 时序问题报错崩掉:调用方
// resolveMaskedExpr 拿到 unresolved=true 后会保守地当成需要脱敏处理,宁可多脱敏一列,也不能
// 打崩查询。
func (dp *dataProcessor) resolveLazyField(sd *db.SelectDataset, r lazyMaskResolver) (field req.ResField, unresolved bool, err error) {
codeResource, code := r.lazyMaskField()
var res req.Resource
if codeResource == "" || codeResource == dp.res.GetCode() || codeResource == dp.fromAlias(sd) {
// 裸列名,或者 codeResource 就是当前资源自己(不管是 dp.res 自己的 code,还是这次查询给它
// 起的 FROM 别名):这两种情况都不是真的 JOIN 进来的字段,直接用 dp.res,完全不用碰
// req.GetResource——DataListField 之类的配置记录里,CodeResource 对"master/自身资源"的
// 字段本来就常常直接填资源自己的 code,不能一律当成外部资源硬查 DI,否则会把当前资源自己
// 的所有字段都误判成"需要走 JOIN 解析",一旦 req.GetResource 暂时不可用,会把整行字段
// 全部误脱敏,而不是只脱敏真的配置了 Roles 的那几个。
res = dp.res
} else {
// 到这里 codeResource 已经不是资源自己的标识了,是这次查询里用的 SQL 别名,去 FROM/JOIN
// 子句里当别名搜——只有这个语境下才适合叫 alias。
alias := codeResource
table, ok := dp.resolveJoinTable(sd, alias)
if !ok {
table = alias
}
getResource, err := do.Invoke[req.GetResource](dp.res.container)
if err != nil {
return nil, false, err
}
found, ok := getResource(table)
if !ok {
return nil, false, fmt.Errorf("resx: MaskField 标记的列 %q 找不到对应的资源(别名/表名 %q)", code, table)
}
res = found
}
field, ok := res.GetField(code)
if !ok {
return nil, false, fmt.Errorf("resx: 资源 %q 没有字段 %q", res.GetCode(), code)
}
return field, false, nil
}
// resolveJoinTable 在 sd 的 FROM/JOIN 子句里找别名是 alias 的那一个,返回它的真实表名。
// MaskField("alias.col") 里的 alias 通常是资源自己的 code(约定,但不保证——比如同一个资源自
// 连接时两个别名不可能都等于资源 code),只有找到真实表名,才能用 req.GetResource 的 table 匹配
// 稳定查到资源,不用赌 alias 本身就是 code。
func (dp *dataProcessor) resolveJoinTable(sd *db.SelectDataset, alias string) (string, bool) {
clauses := sd.GetClauses()
if from := clauses.From().Columns(); len(from) > 0 {
if t, ok := dp.aliasedRealTable(from[0], alias); ok {
return t, true
}
}
for _, j := range clauses.Joins() {
if t, ok := dp.aliasedRealTable(j.Table(), alias); ok {
return t, true
}
}
return "", false
}
// aliasIdentOf 从 AliasedExpression 里取出别名字符串,兼容两种起别名的路径(同 fromAlias 的说明:
// 落在 GetAs() 的 Table 字段还是 Col 字段,取决于当初是直接在 identifier 上调 As(),还是像
// GetTableExpr() 那样先包一层字面量再 As())。
func (dp *dataProcessor) aliasIdentOf(as exp.IdentifierExpression) string {
if t := as.GetTable(); t != "" {
return t
}
if c, ok := as.GetCol().(string); ok {
return c
}
return ""
}
// aliasedRealTable 如果 e 是别名为 alias 的表引用,返回它包着的真实表名。
func (dp *dataProcessor) aliasedRealTable(e exp.Expression, alias string) (string, bool) {
aliased, ok := e.(exp.AliasedExpression)
if !ok || dp.aliasIdentOf(aliased.GetAs()) != alias {
return "", false
}
return dp.realTableOf(aliased.Aliased())
}
// realTableOf 从"被起别名的那个表达式"里取出真实表名,不管别名是什么。兼容两种构造路径:
// db.T(table).As(alias) 时 Aliased() 直接就是原始 identifierres.GetTableExpr().As(alias)
// GetTableExpr 用 db.V(db.T(table)) 包了一层字面量)时 Aliased() 是字面量,真实的 identifier
// 在它的 Args()[0] 里。
func (dp *dataProcessor) realTableOf(e exp.Expression) (string, bool) {
switch v := e.(type) {
case exp.IdentifierExpression:
return v.GetTable(), true
case exp.LiteralExpression:
args := v.Args()
if len(args) != 1 {
return "", false
}
if ident, ok := args[0].(exp.IdentifierExpression); ok {
return ident.GetTable(), true
}
}
return "", false
}
// isDefaultSelect 判断 SELECT 列表是不是"选当前资源自己的全部列":要么是裸的 db.Star()
// clauses.IsDefaultSelect()),要么是显式限定到当前资源自己别名的 db.T(alias).All()——两种
// 写法效果一样,都应该走同一套"展开成显式字段列表"的逻辑。限定到别的(比如 JOIN 进来的)别名
// 的 T().All() 不算,那是另一个资源的全部列,这里够不到。
func (dp *dataProcessor) isDefaultSelect(sd *db.SelectDataset) bool {
clauses := sd.GetClauses()
if clauses.IsDefaultSelect() {
return true
}
cols := clauses.Select().Columns()
if len(cols) != 1 {
return false
}
ident, ok := cols[0].(exp.IdentifierExpression)
if !ok {
return false
}
lit, ok := ident.GetCol().(exp.LiteralExpression)
if !ok || lit.Literal() != "*" {
return false
}
return ident.GetTable() == "" || ident.GetTable() == dp.fromAlias(sd)
}
// fromAlias 取出 FROM 子句里当前资源自己的别名(GetDBTable 默认用资源 code 做别名,也可能被
// ResOption 的 Alias 覆盖),取不到就返回空字符串。
//
// 别名字符串具体落在 GetAs() 的 Table 字段还是 Col 字段,取决于当初是怎么 As() 出来的:
// GetTableExpr() 内部用 db.V(db.T(table)) 包了一层字面量,literal.As() 走的是
// NewAliasExpression -> ParseIdentifier,单个词(不带 ".")会被解析成 col,不是 table;而
// JOIN 场景常见的 db.T(code).As(alias) 是直接在 identifier 上调用 As(),走的是另一条路径,
// 会把别名存进 table 字段。两种都要认。
func (dp *dataProcessor) fromAlias(sd *db.SelectDataset) string {
table := sd.GetClauses().From().Columns()[0]
aliased, ok := table.(exp.AliasedExpression)
if !ok {
return ""
}
return dp.aliasIdentOf(aliased.GetAs())
}
func (dp *dataProcessor) beforeUpdateDataset(ud *db.UpdateDataset) error {
if dp.res.IsVirtual() {
return nil
}
sub, ex := dp.applyIntercept(ResEventUpdate)
if ex != nil {
*ud = *ud.Where(ex)
} else if sub != nil {
table := ud.GetClauses().Table()
if alias, ok := table.(exp.AliasedExpression); ok {
*ud = *ud.Where(alias.GetAs().Col("id").In(sub.Select(db.T(dp.res.GetCode()).Col("id"))))
} else {
*ud = *ud.Where(db.L("?.id", table).In(sub.Select(db.T(dp.res.GetCode()).Col("id"))))
}
}
if dp.onChange != nil {
dp.captureSnapshot(ud.GetClauses().Where())
}
if ud.GetClauses().HasSetValues() {
udv := ud.GetClauses().SetValues()
var data db.Record
switch v := udv.(type) {
case map[string]any:
data = v
case db.Record:
data = v
default:
return fmt.Errorf("unsupported type: %T", udv)
}
if err := dp.normalizeSaveValue(data); err != nil {
return err
}
if dp.onChange != nil {
dp.buildChanges(data)
}
}
return nil
}
func (dp *dataProcessor) beforeDeleteDataset(dd *db.DeleteDataset) error {
if dp.res.IsVirtual() {
return nil
}
sub, ex := dp.applyIntercept(ResEventDelete)
if ex != nil {
*dd = *dd.Where(ex)
} else if sub != nil {
table := dd.GetClauses().From().Columns()[0]
if alias, ok := table.(exp.AliasedExpression); ok {
*dd = *dd.Where(alias.GetAs().Col("id").In(sub.Select(db.T(dp.res.GetCode()).Col("id"))))
} else {
*dd = *dd.Where(db.L("?.id", table).In(sub.Select(db.T(dp.res.GetCode()).Col("id"))))
}
}
if dp.onChange != nil {
dp.captureSnapshot(dd.GetClauses().Where())
}
return nil
}