123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- 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 {
- 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
- }
|