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/base(contracts/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 设置的话默认等于 code,uuid 不显式用 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) }