package rest import ( "encoding/json" "errors" "reflect" "strings" "time" "git.nspix.com/golang/micro/helper/utils" lru "github.com/hashicorp/golang-lru" "gorm.io/gorm" "gorm.io/gorm/clause" "gorm.io/gorm/schema" ) var ( ErrInvalidModelInstance = errors.New("invalid model instance") timeKind = reflect.TypeOf(time.Time{}).Kind() timePtrKind = reflect.TypeOf(&time.Time{}).Kind() schemaCache, _ = lru.New(512) DefaultNamespace = "default" schemaDB *gorm.DB ) const ( ScenarioCreate = "create" ScenarioUpdate = "update" ScenarioDelete = "delete" ScenarioSearch = "search" ScenarioExport = "export" ScenarioList = "list" ScenarioView = "view" ScenarioMapping = "mapping" Basic = "basic" Advanced = "advanced" MatchExactly = "exactly" //精确匹配 MatchFuzzy = "fuzzy" //模糊匹配 ) type ( MigrateOptions struct { TableOptions string Callback func(schema *Schema) (err error) } MigrateOption func(o *MigrateOptions) Rule struct { //规则配置 Min int `json:"min"` Max int `json:"max"` Type string `json:"type"` Unique bool `json:"unique"` Required []string `json:"required"` Regular string `json:"regular"` } FieldValue struct { Label string `json:"label"` Value interface{} `json:"value"` Color string `json:"color"` } visibleCond struct { Column string `json:"column"` Values []interface{} `json:"values"` } liveAttribute struct { //值容器 Enable bool `json:"enable"` Type string `json:"type"` Url string `json:"url"` Columns []string `json:"columns"` } Properties struct { //属性配置 Match string `json:"match"` //匹配模式 PrimaryKey bool `json:"primary_key"` //是否为主键 DefaultValue string `json:"default_value"` //默认值 Readonly []string `json:"readonly"` //只读场景 Disable []string `json:"disable"` //禁用场景 Visible []visibleCond `json:"visible"` //可见条件 Values []FieldValue `json:"values"` //值 Live liveAttribute `json:"live"` //延时加载配置 Icon string `json:"icon"` //显示图标 Suffix string `json:"suffix"` //追加内容 Tooltip string `json:"tooltip"` //字段提示信息 Description string `json:"description"` //字段说明信息 } Schema struct { //Schema的表 Id uint64 `json:"id" gorm:"primary_key"` CreatedAt int64 `json:"created_at" gorm:"autoCreateTime"` //创建时间 UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime"` //更新时间 Namespace string `json:"namespace" gorm:"column:namespace;type:char(60);index"` //域 Module string `json:"module" gorm:"column:module_name;type:varchar(60);index"` //模块名称 Table string `json:"table" gorm:"column:table_name;type:varchar(120);index"` //表名称 Enable uint8 `json:"enable" gorm:"column:enable;type:int(1)"` //是否启用 Tag string `json:"tag" gorm:"column:tag;type:varchar(60)"` //字段归类 Column string `json:"column" gorm:"type:varchar(120)"` //字段名称 Label string `json:"label" gorm:"type:varchar(120)"` //显示名称 Type string `json:"type" gorm:"type:varchar(120)"` //字段类型 Format string `json:"format" gorm:"type:varchar(120)"` //字段格式 Native uint8 `json:"native" gorm:"type:int(1)"` //是否为原生字段 PrimaryKey uint8 `json:"primary_key" gorm:"type:int(1)"` //是否为主键 Expression string `json:"expression" gorm:"type:varchar(526)"` //计算规则 Rules string `json:"rules" gorm:"type:varchar(2048)"` //字段规则 Scenarios string `json:"scenarios" gorm:"type:varchar(120)"` //场景 Properties string `json:"properties" gorm:"type:varchar(4096)"` //字段属性 Position int `json:"position"` //字段排序位置 properties *Properties } Field struct { Column string `json:"column" gorm:"type:varchar(120)"` Label string `json:"label" gorm:"type:varchar(120)"` Type string `json:"type" gorm:"type:varchar(120)"` Format string `json:"format" gorm:"type:varchar(120)"` Scenario string `json:"scenario" gorm:"type:varchar(120)"` Rules string `json:"rules" gorm:"type:varchar(2048)"` Options string `json:"options" gorm:"type:varchar(4096)"` } ) func (r *Rule) String() string { buf, _ := json.Marshal(r) return string(buf) } // getProperties 获取属性 func (schema *Schema) getProperties() *Properties { if schema.properties == nil { schema.properties = &Properties{} _ = json.Unmarshal([]byte(schema.Properties), schema.properties) } return schema.properties } func dataTypeOf(field *schema.Field) string { var dataType string reflectType := field.FieldType for reflectType.Kind() == reflect.Ptr { reflectType = reflectType.Elem() } dataValue := reflect.Indirect(reflect.New(reflectType)) switch dataValue.Kind() { case reflect.Bool: dataType = "boolean" case reflect.Int8, reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: dataType = "integer" case reflect.Float32, reflect.Float64: dataType = "double" case reflect.Struct: if _, ok := dataValue.Interface().(time.Time); ok { dataType = "string" } default: dataType = "string" } return dataType } //获取字段格式 func dataFormatOf(field *schema.Field) string { var dataType string dataType = field.Tag.Get("format") if dataType != "" { return dataType } //如果有枚举值,直接设置为下拉类型 enum := field.Tag.Get("enum") if enum != "" { return "dropdown" } reflectType := field.FieldType for reflectType.Kind() == reflect.Ptr { reflectType = reflectType.Elem() } //时间处理 if utils.InArray(field.Name, []string{"CreatedAt", "UpdatedAt", "DeletedAt"}) { return "datetime" } dataValue := reflect.Indirect(reflect.New(reflectType)) switch dataValue.Kind() { case timeKind, timePtrKind: dataType = "datetime" case reflect.Bool: dataType = "boolean" case reflect.Int8, reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: dataType = "integer" case reflect.Float32, reflect.Float64: dataType = "decimal" case reflect.Struct: if _, ok := dataValue.Interface().(time.Time); ok { dataType = "datetime" } default: dataType = "string" } return dataType } func createStatement(db *gorm.DB) *gorm.Statement { return &gorm.Statement{ DB: db, ConnPool: db.Statement.ConnPool, Context: db.Statement.Context, Clauses: map[string]clause.Clause{}, } } // visibleSchemas 获取某个场景下面的字段 func visibleSchemas(namespace, modelName, tableName, scenario string) (schemas []*Schema) { schemas, _ = getSchemas(namespace, modelName, tableName) values := make([]*Schema, 0) for _, scm := range schemas { if scm.Enable != 1 { continue } if scm.PrimaryKey == 1 { values = append(values, scm) } else { if strings.Contains(scm.Scenarios, scenario) { values = append(values, scm) } } } return values } // getSchemas 获取某个模型下面所有的字段配置 func getSchemas(namespace, moduleName, tableName string) (schemas []*Schema, err error) { schemas = make([]*Schema, 0) cacheKey := namespace + ":" + tableName + "@" + moduleName if v, ok := schemaCache.Get(cacheKey); ok { return v.([]*Schema), nil } if len(namespace) == 0 { namespace = DefaultNamespace } if err = schemaDB.Where("`namespace`=? AND `module_name`=? AND `table_name`=?", namespace, moduleName, tableName).Order("position,id ASC").Find(&schemas).Error; err == nil { //修改表结构缓存 if len(schemas) > 0 { schemaCache.Add(cacheKey, schemas) } } return } func getSchemasNoCache(db *gorm.DB, namespace, moduleName, tableName string) (schemas []*Schema, err error) { tx := db.Session(&gorm.Session{NewDB: true, SkipHooks: true}) if len(namespace) == 0 { namespace = DefaultNamespace } err = tx.Where("`namespace`=? AND `module_name`=? AND `table_name`=?", namespace, moduleName, tableName).Order("position,id ASC").Find(&schemas).Error return } // invalidCache 删除表结构缓存 func invalidCache(namespace, moduleName, tableName string) { cacheKey := namespace + ":" + tableName + "@" + moduleName schemaCache.Remove(cacheKey) } // generateFieldRule 生成数据校验规则 func generateFieldRule(field *schema.Field) string { r := &Rule{ Required: []string{}, } if field.GORMDataType == schema.String { r.Max = field.Size } if field.GORMDataType == schema.Int || field.GORMDataType == schema.Float || field.GORMDataType == schema.Uint { r.Max = field.Scale } if field.NotNull { r.Required = []string{ScenarioCreate, ScenarioUpdate} } if field.PrimaryKey { r.Unique = true } return r.String() } // generateFieldProperties 生成数据属性 func generateFieldProperties(field *schema.Field) string { attr := &Properties{ Match: MatchFuzzy, PrimaryKey: field.PrimaryKey, DefaultValue: field.DefaultValue, Readonly: []string{}, Disable: []string{}, Visible: nil, Values: make([]FieldValue, 0), Live: liveAttribute{}, Description: field.DBName, } //赋值属性 props := field.Tag.Get("props") if props != "" { vs := strings.Split(props, ";") for _, str := range vs { kv := strings.SplitN(str, ":", 2) if len(kv) != 2 { continue } switch strings.ToLower(kv[0]) { case "icon": attr.Icon = kv[1] case "suffix": attr.Suffix = kv[1] case "tooltip": attr.Tooltip = kv[1] case "description": attr.Description = kv[1] case "live": //在线数据解析 ss := strings.Split(kv[1], ",") for _, s := range ss { if len(s) == 0 { continue } attr.Live.Enable = true if s == "dropdown" || s == "cascader" { attr.Live.Type = s } else if s[0] == '/' || strings.HasPrefix(s, "http") { attr.Live.Url = s } else { if strings.IndexByte(s, '.') > -1 { attr.Live.Columns = strings.Split(s, ".") } } } } } } //赋值枚举值 enumns := field.Tag.Get("enum") if enumns != "" { vs := strings.Split(enumns, ";") for _, str := range vs { kv := strings.SplitN(str, ":", 2) if len(kv) != 2 { continue } fv := FieldValue{Value: kv[0]} //颜色分隔符 if pos := strings.IndexByte(kv[1], '#'); pos > -1 { fv.Label = kv[1][:pos] fv.Color = kv[1][pos:] } else { fv.Label = kv[1] } attr.Values = append(attr.Values, fv) } } if !field.Creatable { attr.Disable = append(attr.Disable, ScenarioCreate) } if !field.Updatable { attr.Disable = append(attr.Disable, ScenarioUpdate) } attr.Tooltip = field.Comment if buf, err := json.Marshal(attr); err == nil { return string(buf) } return "" } // generateFieldScenario 生成字段应用场景 func generateFieldScenario(field *schema.Field) string { var ss []string if v, ok := field.Tag.Lookup("scenarios"); ok { v = strings.TrimSpace(v) if v != "" { ss = strings.Split(v, ";") } } else { if field.PrimaryKey { ss = []string{ScenarioList, ScenarioView, ScenarioExport} } else if field.Name == "CreatedAt" || field.Name == "UpdatedAt" { ss = []string{ScenarioList} } else if field.Name == "DeletedAt" || field.Name == "Namespace" { //不添加任何显示场景 ss = []string{} } else { //高级字段只配置一些简单的场景 if field.Tag.Get("tag") == Advanced { ss = []string{ScenarioCreate, ScenarioUpdate, ScenarioView, ScenarioExport} } else { ss = []string{ScenarioSearch, ScenarioList, ScenarioCreate, ScenarioUpdate, ScenarioView, ScenarioExport} } } } if buf, err := json.Marshal(ss); err == nil { return string(buf) } return "" } // migrate 合并数据表结构 func migrateUp(namespace string, value interface{}, opts *MigrateOptions) (err error) { var ( pos int ok bool model Model moduleName string tableName string stmt *gorm.Statement scm *Schema schemas []*Schema values []*Schema field *schema.Field columnIsExists bool columnName string columnLabel string ) if schemaDB == nil { return errors.New("call initSchema first") } if len(namespace) == 0 { namespace = DefaultNamespace } stmt = createStatement(schemaDB) if value == nil { return ErrInvalidModelInstance } if model, ok = value.(Model); !ok { return ErrInvalidModelInstance } moduleName = model.ModuleName() tableName = model.TableName() if err = stmt.Parse(value); err != nil { return err } if schemas, err = getSchemas(namespace, moduleName, tableName); err != nil { schemas = make([]*Schema, 0) } totalCount := len(stmt.Schema.Fields) //遍历所有字段 for _, field = range stmt.Schema.Fields { columnName = field.DBName if columnName == "-" { continue } if columnName == "" { columnName = field.Name } columnIsExists = false for _, scm = range schemas { if scm.Column == columnName { columnIsExists = true break } } if columnIsExists { continue } columnLabel = field.Tag.Get("comment") if columnLabel == "" { columnLabel = strings.Join(utils.BreakUp(field.DBName), " ") } isPrimaryKey := uint8(0) if field.PrimaryKey { isPrimaryKey = 1 } pv := pos //默认把创建时间和更新时间放到最后 if field.Name == "CreatedAt" || field.Name == "UpdatedAt" || field.Name == "DeletedAt" { pv = totalCount } tag := field.Tag.Get("tag") if tag == "" { tag = Basic } schemaModel := &Schema{ Namespace: namespace, Module: moduleName, Table: tableName, Enable: 1, Tag: tag, Column: columnName, Label: columnLabel, Type: strings.ToLower(dataTypeOf(field)), Format: strings.ToLower(dataFormatOf(field)), Native: 1, PrimaryKey: isPrimaryKey, Rules: generateFieldRule(field), Scenarios: generateFieldScenario(field), Properties: generateFieldProperties(field), Position: pv, } if opts.Callback != nil { if err = opts.Callback(schemaModel); err == nil { values = append(values, schemaModel) pos++ } } else { values = append(values, schemaModel) pos++ } } if len(values) > 0 { //batch save err = schemaDB.Create(values).Error } return } func initSchema(db *gorm.DB) (err error) { schemaDB = db.Session(&gorm.Session{NewDB: true}) err = schemaDB.AutoMigrate(&Schema{}) return }