Files
req/resx/res_query_field.go
what 09f4c40d67 新增 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 资源实现包。
2026-07-22 09:11:50 +08:00

169 lines
4.9 KiB
Go

package resx
import (
"reflect"
"strings"
"unicode"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/exp"
"git.fsdpf.net/go/req"
)
// QueryField 是 req.QueryField 的具体实现。原来放在 orm-v2,这里搬过来跟 ResField 是同一个原因:
// ToSql() 需要用 MaskField 按字段的 Roles 做脱敏标记,放在 resx 里可以直接调用,不用 orm 包再反过来
// import resx。orm-v2/contracts-v2 base 的 QueryField 现在都是指向这里的类型别名,兼容原有引用。
//
// 嵌入的是 req.ResField 接口而不是具体的 ResField 结构体:调用方经常手上已经有一个真实的
// req.ResField(比如从 resource.GetField(code) 查出来的),这时候直接传它本身即可,不用先转换成
// 具体类型(也不用为了绕开"没有的字段"再拼一个大部分为空的 ResField 出来)。QueryField 自己的方法
// 全程只通过 req.ResField 接口访问底层字段,不需要具体类型独有的能力。
type QueryField struct {
req.ResField
typ req.RouteParamType `db:"dataType"`
alias string `db:"alias"`
isExpr bool `db:"isExpr"`
isOmitempty bool
ignored bool
}
// Alias 返回字段的别名。
func (this QueryField) Alias() string {
return this.alias
}
// Ignored 返回字段是否被忽略。
func (this QueryField) Ignored() bool {
return this.ignored
}
// IsExpr 返回字段是否为表达式。
func (this QueryField) IsExpr() bool {
return this.isExpr
}
// IsOmitempty 返回字段是否在为空时忽略。
func (this QueryField) IsOmitempty() bool {
return this.isOmitempty
}
// Type 返回字段的数据类型。
func (this QueryField) Type() req.RouteParamType {
return this.typ
}
// SetOptions 设置字段选项(表达式、忽略空值、忽略等)。
func (this *QueryField) SetOptions(options byte) req.QueryField {
if options&req.IsExpr != 0 {
this.isExpr = true
}
if options&req.IsOmitempty != 0 {
this.isOmitempty = true
}
if options&req.Ignored != 0 {
this.ignored = true
}
return this
}
// GetCodeOrAlias 返回字段的别名(如果有),否则返回字段代码。
func (this QueryField) GetCodeOrAlias() string {
if this.Alias() != "" {
return this.Alias()
}
return this.GetCode()
}
// ToSql 将查询字段转换为SQL表达式。
func (this *QueryField) ToSql() db.Expression {
if this.Alias() != "" {
if this.IsExpr() {
if this.GetCode() == "" {
return db.V(nil).As(this.Alias())
}
return db.L("(?)", db.L(this.GetCode())).As(this.Alias())
}
return this.col().As(this.Alias())
}
return this.col()
}
// col 构造这个字段对应的列引用,一律用 MaskField(...) 标记。这里不能靠 this.GetRoles() 提前判断
// 要不要标记——构造 QueryField 的这个 ResField 经常是从 DataListField/ResApiField 之类的配置记录
// 现造出来的,根本不带 Roles(这些配置结构体压根没有 Roles 这一列),如果在这里用"当前手上这个
// ResField 有没有 Roles"来决定要不要包 MaskField,等于默认永远不脱敏,绕过了字段级权限。
// 真正的判断交给 dataProcessor.maskSelect:查询真正执行前,从 SELECT 列表所在的 FROM/JOIN 子句里
// 把 GetCodeResource() 这个别名解析成真实表名,再查真实资源上对应字段的真实 Roles——这一步不需要
// 构造 QueryField 时就知道 Roles。
func (this *QueryField) col() exp.IdentifierExpression {
return MaskField(this.GetCodeResource() + "." + this.GetCode())
}
// ToStructField 将查询字段转换为反射结构体字段定义。
func (this *QueryField) ToStructField(tags ...string) reflect.StructField {
var typ reflect.Type
fCode := this.GetCodeOrAlias()
// 字段规则
if !unicode.IsLetter(rune(fCode[0])) {
panic("struct field name invalid. " + fCode)
}
fTag := `db:"` + fCode
if this.ignored {
fTag = fTag + `" json:"-"`
} else if this.isOmitempty {
fTag = fTag + `" json:"` + fCode + `,omitempty"`
} else {
fTag = fTag + `" json:"` + fCode + `"`
}
if len(tags) > 0 {
fTag = strings.Join(tags, " ") + " " + fTag
}
switch this.typ {
case req.ReqString:
typ = reflect.TypeOf("")
case req.ReqNumber:
typ = reflect.TypeOf(float64(0))
case req.ReqInteger:
typ = reflect.TypeOf(int64(0))
case req.ReqFloat:
typ = reflect.TypeOf(float64(0))
case req.ReqBool:
typ = reflect.TypeOf(true)
case req.ReqArray:
typ = reflect.TypeOf([]any{})
case req.ReqJson:
typ = reflect.TypeOf(map[string]any{})
}
return reflect.StructField{
Name: strings.ToUpper(fCode[:1]) + fCode[1:],
Tag: reflect.StructTag(fTag),
Type: typ,
}
}
// NewQueryField 创建并返回一个新的查询字段实例。
// 参数 rField 是资源字段,t 是字段类型,opts 是可选的配置选项。
func NewQueryField(rField req.ResField, t req.RouteParamType, opts ...QueryFieldOption) req.QueryField {
qf := &QueryField{
ResField: rField,
typ: t,
}
for _, opt := range opts {
opt(qf)
}
return qf
}