新增 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
+355
View File
@@ -0,0 +1,355 @@
package resx
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/schema"
"git.fsdpf.net/go/req"
"github.com/spf13/cast"
)
// ResField 是 req.ResField 的具体实现,也是 QueryField 的底子(QueryField 匿名嵌入它)。
// 原来放在 contracts-v2/base,这里搬过来是因为 resx 自己(MaskField 的懒解析、QueryField.ToSql
// 的字段级脱敏判断)需要用到它,而 resx 不能反过来 import contracts/basecontracts/base 依赖
// contracts 根包,根包又依赖 resx,会成环)。contracts-v2/base.ResField 现在是指向这里的类型
// 别名,兼容原有引用。
//
// 字段全部私有,只能通过 NewResField(opts...) + FieldWith* 构造——避免调用方绕开
// GetRoles()/ToValue() 等方法直接摆弄底层数据。JSON 序列化/反序列化(资源缓存从数据库
// JSON_ARRAYAGG 结果里加载字段配置,见 framework-v2 的 init_res_cache.go)靠手写的
// MarshalJSON/UnmarshalJSON 维持原来的 wire 格式,不依赖 encoding/json 对导出字段的默认反射。
type ResField struct {
uuid string
name string
code string
codeResource string
dataType req.ResDataType
length string
comment string
defaultValue string
virtual bool
virtualExpr string
// roles 配置了"能读写该字段真实值"的角色列表,为空表示不限制。见 req.ResField.GetRoles 注释。
roles []string
}
// NewResField 创建一个 ResField。code/codeResource 是必填的位置参数(每一个字段都要设置,不适合
// 当可选项);name 不显式用 FieldWithName 设置的话默认等于 codeuuid 不显式用 FieldWithUuid 设置
// 的话默认是 codeResource+code——这两个字段在测试和一些一次性计算字段的场景里经常不关心具体取值,
// 只是不能为空,给个确定性的默认值省得每次都要传。dataType 放进 FieldWithDataType 而不是位置参数:
// 很多调用点(临时拼一个查询字段、只关心 Code/CodeResource)并不知道或不关心具体数据类型,当位置
// 参数的话每次都要传个占位值,不如做成可选项。FieldWith* 选项定义在 res_field_option.go。
func NewResField(code, codeResource string, opts ...ResFieldOption) req.ResField {
f := ResField{
code: code,
codeResource: codeResource,
name: code,
uuid: codeResource + code,
}
for _, opt := range opts {
opt(&f)
}
return f
}
func (this ResField) ToStructField(tags ...string) reflect.StructField {
var typ reflect.Type
fCode := this.code
fTag := `db:"` + fCode + `" json:"` + fCode + `"`
if len(tags) > 0 {
fTag = strings.Join(tags, " ") + " " + fTag
}
switch this.dataType {
case req.ResString, req.ResText, req.ResEnum,
req.ResTimestamp, req.ResDate, req.ResDatetime:
typ = reflect.TypeOf(string(""))
case req.ResInteger, req.ResSmallInteger:
typ = reflect.TypeOf(int64(0))
case req.ResDecimal:
typ = reflect.TypeOf(float64(0))
case req.ResBoolean:
typ = reflect.TypeOf(true)
case req.ResJson:
if this.defaultValue != "" && this.defaultValue[0:1] == "[" {
typ = reflect.TypeOf([]any{})
} else {
typ = reflect.TypeOf(map[string]any{})
}
case req.ResVector:
typ = reflect.TypeOf([]float64{})
}
return reflect.StructField{
Name: strings.ToUpper(fCode[:1]) + fCode[1:],
Tag: reflect.StructTag(fTag),
Type: typ,
}
}
func (this ResField) IsVirtual() bool {
return this.virtual
}
func (this ResField) GetRoles() []string {
return this.roles
}
func (this ResField) GetUuid() string {
return this.uuid
}
func (this ResField) GetCode() string {
return this.code
}
func (this ResField) GetCodeResource() string {
return this.codeResource
}
func (this ResField) GetName() string {
return this.name
}
func (this ResField) GetDataType() req.ResDataType {
return this.dataType
}
func (this ResField) GetLength() string {
return this.length
}
func (this ResField) GetComment() string {
return this.comment
}
func (this ResField) GetDefault() string {
return this.defaultValue
}
func (this ResField) GetVirtualExpr() string {
return this.virtualExpr
}
func (this ResField) GetQueryDataType() req.RouteParamType {
switch this.GetDataType() {
case req.ResEnum,
req.ResTimestamp, req.ResDate,
req.ResDatetime, req.ResString,
req.ResText:
return req.ReqString
case req.ResInteger, req.ResSmallInteger:
return req.ReqInteger
case req.ResDecimal:
return req.ReqFloat
case req.ResBoolean:
return req.ReqBool
case req.ResJson:
if this.defaultValue != "" && this.defaultValue[0:1] == "[" {
return req.ReqArray
}
return req.ReqJson
case req.ResVector:
return req.ReqArray
}
return req.ReqString
}
func (this ResField) ToValue(v any) any {
if this.dataType == req.ResJson {
if v == nil {
if this.defaultValue != "" && this.defaultValue[0:1] == "[" {
return db.V("[]")
} else if this.defaultValue != "" && this.defaultValue[0:1] == "{" {
return db.V("{}")
} else if this.defaultValue == "" {
return db.V("{}")
}
return this.GetRawDefault()
}
if str, ok := v.(string); ok {
return str
} else if raw, ok := v.(db.Expression); ok {
return raw
} else if b, err := json.Marshal(v); err == nil {
return string(b)
} else {
panic(fmt.Sprintf("%s, 类型转换错误, %s", this.code, err))
}
}
if this.dataType == req.ResVector {
if v == nil {
return db.V(nil)
}
var parts []string
switch floats := v.(type) {
case []float32:
parts = make([]string, len(floats))
for i, f := range floats {
parts[i] = strconv.FormatFloat(float64(f), 'g', -1, 32)
}
case []float64:
parts = make([]string, len(floats))
for i, f := range floats {
parts[i] = strconv.FormatFloat(f, 'g', -1, 64)
}
default:
return v
}
return db.L("[" + strings.Join(parts, ", ") + "]")
}
return v
}
func (this ResField) GetRawDefault() db.Expression {
if this.dataType == req.ResJson {
if this.defaultValue != "" && this.defaultValue[0:1] == "[" {
return db.V("[]")
} else if this.defaultValue != "" && this.defaultValue[0:1] == "{" {
return db.V("{}")
} else if this.defaultValue == "" {
return db.V("{}")
}
} else if this.dataType == req.ResBoolean {
if v, _ := strconv.ParseBool(this.defaultValue); v {
return db.V(true)
}
return db.V(false)
}
if len(this.defaultValue) > 4 && strings.ToLower(this.defaultValue[0:4]) == "sql:" {
return db.L(this.defaultValue[4:])
}
if this.defaultValue == "" {
if this.GetDataType() == req.ResDate || this.GetDataType() == req.ResDatetime {
return db.V(nil)
}
return db.V("")
}
if strings.ToUpper(this.defaultValue) == "CURRENT_TIMESTAMP" {
return db.L(this.defaultValue)
}
return db.V(this.defaultValue)
}
func (this ResField) ToBlueprint(table *schema.Blueprint) (temp *schema.ColumnDefinition) {
switch this.code {
case "id":
return table.BigIncrements("id").AutoIncrement().Comment("ID")
case "enabled":
return table.Boolean("enabled").Default("1").Comment("是否有效")
case "created_user":
return table.Char("created_user", 36).Default("00000000-0000-0000-0000-000000000000").Comment("创建者")
case "owned_user":
return table.Char("owned_user", 36).Default("00000000-0000-0000-0000-000000000000").Comment("拥有者")
case "created_at":
return table.Timestamp("created_at").UseCurrent().Comment("创建时间")
case "updated_at":
return table.Timestamp("updated_at").UseCurrent().Default(db.L("ON UPDATE CURRENT_TIMESTAMP")).Comment("更新时间")
case "deleted_at":
return table.DateTime("deleted_at").Nullable().Comment("删除时间")
}
isNull := false
comment := this.name
def := any(this.defaultValue)
if this.comment != "" {
comment += " [ " + strings.Trim(this.comment, `' "`) + " ]"
}
switch this.dataType {
case "string":
len := 255
if v, err := strconv.Atoi(strings.Trim(this.length, `' "`)); err == nil {
len = v
}
temp = table.String(this.code, len)
case "smallInteger":
// integer 默认长度 4
temp = table.SmallInteger(this.code)
case "boolean":
// integer 默认长度 1
temp = table.Boolean(this.code)
case "integer":
// integer 默认长度 11
temp = table.Integer(this.code)
case "date", "dateTime", "timestamp":
if this.dataType == "date" {
temp = table.Date(this.code)
} else {
temp = table.DateTime(this.code)
}
if strings.ToUpper(this.defaultValue) == "SQL:CURRENT_TIMESTAMP" {
def = db.L("CURRENT_TIMESTAMP")
} else if def == "" {
isNull = true
}
case "decimal":
allowed := strings.SplitN(this.length, ",", 2)
total := 8
places := 2
if v, err := strconv.Atoi(strings.Trim(allowed[0], `' "`)); err == nil {
total = v
}
if v, err := strconv.Atoi(strings.Trim(allowed[1], `' "`)); err == nil {
places = v
}
temp = table.Decimal(this.code, total, places)
case "enum":
allowed := []string{}
for _, v := range strings.Split(this.length, ",") {
allowed = append(allowed, strings.Trim(v, `' "`))
}
temp = table.Enum(this.code, allowed)
case "json":
temp = table.Json(this.code)
isNull = true
case "text":
temp = table.Text(this.code)
isNull = true
case "vector":
temp = table.Vector(this.code, cast.ToInt(this.length))
isNull = false
default:
panic(fmt.Sprintf("Unknown type: %s", this.dataType))
}
if this.IsVirtual() {
temp.VirtualAs(this.virtualExpr)
}
if isNull {
temp.Nullable()
} else {
temp.Default(def)
}
temp.Comment(comment)
return temp
}
func (this ResField) ToQueryField(t req.RouteParamType, alias string, options byte) req.QueryField {
o := &QueryField{
ResField: this,
typ: t,
alias: alias,
}
return o.SetOptions(options)
}