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 注释 skipPermission bool // ResOptions.RolesScope 不是 ResScopeOn/ResScopeAll:跳过权限过滤/字段脱敏,见 req.WithRolesScope 注释 } // 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,返回角色权限过滤条件,并记录本次写完后要不要调用、调用谁。 // skipPermission 时丢弃行级权限过滤条件;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.skipPermission { 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 各行的自增 id(MySQL: 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.skipPermission && !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.skipPermission { 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 里有没有任何一个配置了 Roles(hasRestrictedFields 对当前资源自己 // 和每个能解析到的 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 函数参数没法单独起别名。 // // 目前只递归处理 SQLFunctionExpression(db.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.ResField:codeResource // 为空表示当前资源自己的字段,直接用 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() 直接就是原始 identifier;res.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 }