Skip to content

数据模型 ​

Yao 可以读取数据模型定义,实现数据迁移、元数据原子操作、元数据输入校验和元数据管理后台。元数据原子操作方法被映射为处理器(process),支持模型间数据关系映射,可在数据流(Flow)和接口(API)中访问查询。如采用golang语言开发业务插件(Plugin),可以使用 Package Gou 访问模型的各个方法, 后续将提供 NodeJS 等语言 SDK。

1 命名规范 ​

数据模型描述文件是以 小写英文字母 命名的 JSON 文本文件 <name>.mod.json

文件夹 (相对应用模型根目录)文件名模型名称Process (在 API /Flow 中引用)
/name.mod.jsonnamemodels.name.<process>
/group/name.mod.jsongorup.namemodels.gorup.name.<process>
/group1/group2/name.mod.jsongorup1.gorup2.namemodels.gorup1.group2.name.<process>

2 文档结构 ​

数据模型定义文档,由基础信息、数据表、字段定义、索引定义、关系映射、默认数据和配置部分构成。查看完整示例

json
{
  "name": "用户",
  "table": {},
  "columns": [],
  "indexes": [],
  "relations": {},
  "values": [],
  "option": {}
}
字段类型说明必填项
nameString模型中文名称是
tableObject数据表定义是
columnsArray<Object>字段定义是
indexesArray<Object>索引定义是
relations[key:String]:Object关系映射否
valuesArray<Object>默认数据否
optionObject配置选型否

2.1 基础信息 ​

基础信息包含 name 、 description 、version 等字段,主要用于在开发平台中呈现检索。

json
{
  "name": "用户",
  "version": "1.0.0",
  "description": "网站用户元数据模型",
  "table": {},
  "columns": [],
  "indexes": []
}
字段类型说明必填项
nameString中文名称是
versionString版本号 x.x.x 遵循 Semantic Versioning 2.0.0 规范是
tunString象传智慧共享仓库 Tun (tun.iqka.com) 地址 如:microcity/petstore/user否
descriptionString数据模型详细介绍否
authorString数据模型作者否
emailString数据模型作者联系方式否
licenseString数据模型共享协议如 MIT否
homepageString数据模型提供者官网否

2.2 数据表 table ​

数据表格包含 name、comment 等字段,定义模型存储在数据库中的数据表名称、注释等信息。支持 MySQL, PostgreSQL、SQLite等 Xun Database 或第三方提供驱动的数据库。

json
{
  "table": {
    "name": "user",
    "comment": "用户表",
    "engine": "InnoDB"
  }
}
字段类型说明必填项
nameString数据表名称是
commentString数据表注释中文名否
engineString数据表引擎(MySQL ONLY) 许可值 InnoDB, MyISAM否

2.3 字段定义 columns ​

一个模型可以包含多个字段定义,每个字段定义包含 label、name、type、 validations 等信息。

json
{
  "columns": [
    { "label": "ID", "name": "id", "type": "ID" },
    {
      "label": "厂商",
      "name": "manu_id",
      "type": "bigInteger",
      "length": 50,
      "comment": "所属厂商",
      "nullable": true,
      "index": true,
      "validations": [
        {
          "method": "typeof",
          "args": ["integer"],
          "message": "{{input}}类型错误, {{label}}应为数字"
        },
        {
          "method": "min",
          "args": [0],
          "message": "{{label}}应大于0"
        }
      ]
    },
    {
      "label": "手机号",
      "name": "mobile",
      "type": "string",
      "length": 50,
      "comment": "手机号",
      "index": true,
      "crypt": "AES",
      "validations": [
        {
          "method": "typeof",
          "args": ["string"],
          "message": "{{input}}类型错误, {{label}}应该为字符串"
        },
        {
          "method": "pattern",
          "args": ["^1[3-9]\\d{9}$"],
          "message": "{{input}}格式错误"
        }
      ]
    }
  ]
}
字段类型说明必填项
nameString字段名称,对应数据表中字段名称是
typeString字段类型,是
labelString字段显示名称,用于在管理表单,开发平台等成场景下呈现是
commentString字段注释,对应数据表中字段注释否
titleString字段标题,可用于开发平台中呈现否
descriptionString字段介绍,可用于开发平台中呈现否
lengthInteger字段长度,对 string 等类型字段有效否
precisionInteger字段位数(含小数位),对 float、decimal 等类型字段有效否
scaleInteger字段小数位位数,对 float、decimal 等类型字段有效否
optionArray<String>字段许可值,对 enum 类型字段有效否
defaultString|Integer|Float字段默认值否
default_rawString字段默认值,支持数据库函数,如 NOW() default 和 default_raw 同时存在 default_raw 优先级高否
cryptString字段加密存储方式(MySQL Only)。许可值 AES, PASSWORD否
nullableBool字段是否可以为空,默认为 false否
indexBool字段是否为索引,默认为 false否
uniqueBool字段是否为唯一索引,默认为 false , 如为 true 无需同时将 index 设置为 true否
primaryBool字段是否为主键,每张表至多一个主键字段。默认为 false否
validationsArray<Object>字段校验规则否

字段类型 ​

类型说明可选参数MySQL 字段类型
string字符串lengthVARCHAR(length )
char字符lengthCHAR (length )
text文本TEXT
mediumText中文本MEDIUMTEXT
longText长文本LONGTEXT
binary二进制数据VARBINARY
date日期DATE
datetime日期时间lengthDATETIME
datetimeTz带时区的日期时间lengthDATETIME
time时间lengthTIME
timeTz带时区的时间lengthTIME
timestamp时间戳lengthTIMESTAMP
timestampTz带时区的时间戳lengthTIMESTAMP
tinyInteger微整型TINYINT
tinyIncrements无符号微整型+自增TINYINT UNSIGNED AUTO_INCREMENT
unsignedTinyInteger无符号微整型TINYINT UNSIGNED
smallInteger小整型SMALLINT
smallIncrements无符号小整型+自增SMALLINT UNSIGNED AUTO_INCREMENT
unsignedSmallInteger无符号小整型SMALLINT UNSIGNED
integer整型INT
increments无符号整型+自增INT UNSIGNED AUTO_INCREMENT
unsignedInteger无符号整型INT UNSIGNED
bigInteger长整型BIGINT
bigIncrements无符号长整型+自增BIGINT UNSIGNED AUTO_INCREMENT
unsignedBigInteger无符号长整型BIGINT UNSIGNED
id长整型+自增BIGINT UNSIGNED AUTO_INCREMENT
ID长整型+自增(同 id)BIGINT UNSIGNED AUTO_INCREMENT
decimal小数(一般用于存储货币)precision、scaleDECIMAL(precision,scale)
unsignedDecimal无符号小数 (一般用于存储货币)precision、scaleDECIMAL (precision,scale) UNSIGNED
float浮点数precision、scaleFLOAT (precision,scale)
unsignedFloat无符号浮点数precision、scaleFLOAT (precision,scale) UNSIGNED
double双精度precision、scaleDOUBLE (precision,scale)
unsignedDouble无符号双精度precision、scaleDOUBLE (precision,scale) UNSIGNED
boolean布尔型BOOLEAN
enum枚举型optionENUM(option...)
jsonJSON 文本JSON
JSONJSON 文本(同 json)JSON
jsonbJSON (二进制格式存储)JSON
JSONBJSON (二进制格式存储 同 jsonb)JSON
uuidUUID 格式字符串VARCHAR(36)
ipAddressIP 地址INT
macAddressMAC 地址BIGINT
year年份SMALLINT

校验方法 ​

一个字段可以包含多条校验规则,每条校验规则可以选用 min,max, pattern, typeof 等校验方法。

json
{
  "columns": [
    {
      "label": "手机号",
      "name": "mobile",
      "type": "string",
      "length": 50,
      "comment": "手机号",
      "index": true,
      "crypt": "AES",
      "validations": [
        {
          "method": "typeof",
          "args": ["string"],
          "message": "{{input}}类型错误, {{label}}应该为字符串"
        },
        {
          "method": "pattern",
          "args": ["^1[3-9]\\d{9}$"],
          "message": "{{input}}格式错误"
        }
      ]
    }
  ]
}

校验规则定义

字段类型说明必填项
methodString校验方法名称,可选值 typeof, pattern 等是
argsArray<String|Integer|Float>校验方法参数,例如 [20], ["^1[3-9]\\d{9}$"]否
messageString如校验不通过,返回的错误提示。支持使用 {{<name>}} 引用字段信息, 如{{label}}将被替换为字段 label中定义的数值; {{input}} 被替换为用户输入数值。否

校验方法清单 ​

校验方法参数说明示例
typeof[<String>] 许可值 string, integer, float, number, datetime, timestamp数值类型{"method":"typeof", "args":["integer"]}
min[<Integer|Float>]最小值{"method":"min", "args":[20]}
max[<Integer|Float>]最大值{"method":"max", "args":[0.618]}
enum[String...]枚举选项{"method":"enum", "args":["enabled", "disabled"]}
pattern[String]正则匹配{"method":"pattern", "args":["^1[3-9]\\d{9}$"]}
minLength[<Integer>]最小长度{"method":"minLength", "args":[20]}
maxLength[<Integer>]最大长度{"method":"maxLength", "args":[100]}
email[]邮箱{"method":"email", "args":[]}
mobile[<String>] 区域列表(可选), 默认为 cn 许可值 cn,us手机号{"method":"mobile", "args":[]} {"method":"mobile", "args":["us"]}

加密方式 ​

当前支持 AES 和 PASSWORD 两种字段数值加密存储算法,其中 AES 仅支持 MySQL 数据库。

加密算法说明是否可逆
AESAES 加密,需设定 XIANG_DB_AESKEY是
PASSWORDPASSWORD HASH 加密否

保留字 ​

以下名称不能用于字段名称。

保留字说明
created_at用于记录创建时间戳
updated_at用于记录更新时间戳
deleted_at用于记录软删除标记
__restore_data用于软删除时备份唯一字段数值

2.4 索引定义 indexes ​

一个数据模型,可以包含多个索引。对于单一索引,推荐在字段定义时,使用 index 、 unique 和 primary 修饰符定义,对于复合索引或全文检索索引,在 indexes 中定义。

json
{
  "indexes": [
    {
      "comment": "厂商用户",
      "name": "manu_id_mobile_unique",
      "columns": ["manu_id", "mobile"],
      "type": "unique"
    },
    {
      "comment": "简历全文检索",
      "name": "resume_fulltext",
      "columns": ["resume"],
      "type": "fulltext"
    }
  ]
}
字段类型说明必填项
nameString索引名称。命名规范为 字段 1_字段 2_字段 n_索引类型是
typeString索引类型 许可值 index 索引, unique 唯一索引, primary 主键, fulltext 全文检索是
columnsArray<String>关联字段名称列表(顺序有关) ["字段 1","字段 2"] 与 ["字段 2","字段 1"] 不同是
commentString索引注释否

2.5 关系映射 relations ​

当前关系映射部分为 beta 版本, 可能会依据用户使用反馈,调整数据结构

数据模型支持一对一、一对多两种关系映射,可以通过定义映射关系将多个数据模型关联,查询时使用with参数即可同时返回关联模型数据。

映射关系名称关系说明
hasOne一对一模型 A 与模型 B 通过一对一关联
hasMany一对多模型 A 与模型 B 通过一对多关联

关联关系使用 [key:String]:Object Relation 数据结构定义 ( {"关联名称1":{}, "关联名称2":{}}, 关联名称为 小写英文字母 )

在模型文件 user.json 中定义

json
{
  "relations": {
    "manu": {
      "type": "hasOne",
      "model": "manu",
      "key": "id",
      "foreign": "manu_id",
      "query": { "select": ["name", "short_name", "type"] }
    },
    "addresses": {
      "type": "hasMany",
      "model": "address",
      "key": "user_id",
      "foreign": "id",
      "query": {
        "select": ["province", "city", "location", "status"],
        "pagesize": 20
      }
    },
    "mother": {
      "type": "hasOneThrough",
      "links": [
        {
          "type": "hasOne",
          "model": "friends",
          "key": "user_id",
          "foreign": "user.id",
          "query": {
            "select": ["status", "type", "friend_id"],
            "wheres": [
              {
                "column": "type",
                "value": "monther"
              }
            ]
          }
        },
        {
          "type": "hasOne",
          "model": "user",
          "key": "id",
          "foreign": "user_mother_friends.friend_id",
          "query": {
            "select": ["name", "id", "status", "type", "secret", "extra"],
            "withs": {
              "manu": {},
              "roles": {},
              "address": {}
            }
          }
        }
      ]
    },
    "roles": {
      "type": "hasManyThrough",
      "links": [
        {
          "type": "hasMany",
          "model": "user_roles",
          "key": "user_id",
          "foreign": "id",
          "query": {
            "select": ["status"],
            "pagesize": 20
          }
        },
        {
          "type": "hasOne",
          "model": "role",
          "key": "id",
          "foreign": "role_id",
          "query": {
            "select": ["name", "label", "permission"]
          }
        }
      ]
    }
  }
}

Object Relation

字段类型说明必填项
typeString关系类型 许可值 hasOne, hasOneThrough , hasMany , hasManyThrough是
keyString关联模型的关联字段名称否
modelString关联模型名称否
foreignString当前模型的关联字段名称否
queryObject QueryParam关系查询参数默认值。如在查询时未指定关联查询参数,则替使用在模型中定义的查询参数否
linksArray<Object Relation>hasOneThrough 或 hasManyThrough 多表关联关系定义否

2.5.1 hasOne 一对一 ​

数据模型 user 数据表结构如下:

字段类型说明
idID用户 ID
manu_idbigInteger所属厂商 ID
namestring姓名

数据模型 manu 数据表结构如下:

字段类型说明
idID厂商 ID
shortstring厂商简称
companystring公司名称

在查询用户数据时,可以同时列出厂商信息或可以按关联厂商进行查询,则可以在定义 user 数据模型时,设定与 manu 数据模型关系.

在模型文件 user.json 中定义

json
{
  "name": "用户",
  "relations": {
    "manu": {
      "type": "hasOne",
      "model": "manu",
      "key": "id",
      "foreign": "manu_id",
      "query": { "select": ["short", "company"] }
    }
  }
}

说明

1.将关系映射类型指定为 hasOne

2.将关联模型 model 设置为 manu

3.将关联模型 key 设置为 id, 即:manu.id 引擎处理时,自动关联 manu 表的 id 字段。

4.将 foreign 设置为 manu_id, 即: user.manu_id 引擎处理时,将 manu.id 和 user.manu_id 关联

5.可以在 query 字段中,设置默认的查询条件,如指定读取的字段等。

引擎解析后的 SQL 为:

sql
SELECT `user`.*,
  `manu`.`short` AS `user_manu_short`,
  `manu`.`company` AS `user_manu_company`,
  FROM `user` AS `user`
  LEFT JOIN `manu` as `user_manu` ON `user_manu`.`id` = `user`.`manu_id`

访问

在调用 process 查询时,传入 with 参数,即可同时返回厂商信息

bash
GET  /api/user/find/1?with=manu&manu.select=id,short

2.5.2 hasMany 一对多 ​

数据模型 user 数据表结构如下:

字段类型说明
idID用户 ID
manu_idbigInteger所属厂商 ID
namestring姓名

数据模型 address 数据表结构如下:

字段类型说明
idID地址 ID
user_idbigInteger所属用户 ID (关联 user.id )
locationstring详细地址

对于类似一个用户有多个通信地址的业务场景,可以通过建立一对多的映射关系来实现。

在模型文件 user.json 中定义

json
{
  "name": "用户",
  "relations": {
    "addresses": {
      "type": "hasMany",
      "model": "address",
      "key": "user_id",
      "foreign": "id",
      "query": {
        "select": ["location"],
        "limit": 20
      }
    },
}

说明

1.将关系映射类型指定为 hasMany

2.将关联模型 model 设置为 address

3.将关联模型 key 设置为 user_id, 即:address.user_id 引擎处理时,自动关联 address 表的 user_id 字段。

4.将 foreign 设置为 id, 即: user.id 引擎处理时,将 user.id 和 address.user_id 关联

5.可以在 query 字段中,设置默认的查询条件,如指定读取的字段等。对于 hasMany 建议设置默认 limit 约束返回数据条目

对于 hasMany 类型关系映射,引擎将分两次查询。首次查询出主模型以及关联的 ID 列表,第二次根据 ID 列表,查询关联数据信息。

第一次查询:

sql
SELECT `user`.* FROM `user` AS `user`

引擎处理结果,并读取 user.id

第二次查询:

sql
SELECT  `address`.`user_id`, `address`.`location` FROM `address` AS `address`
  WHERE `address`.`user_id` IN (<user.id...>)

引擎处理结果,关联用户地址信息

访问

在调用 process 查询时,传入 with 参数,即可同时取得 addresses 的关联信息

bash
GET  /api/user/find/1?with=addresses

2.7 配置选项 option ​

在 option 中设定模型配置参数

json
{
  "name": "地址",
  "option": {
    "timestamps": true,
    "soft_deletes": true
  }
}
选项类型说明
timestampsBool为 true 时, 自动创建 created_at、updated_at 字段,并在插入和更新数据时,标记对应操作时间
soft_deletesBool为 true 时, 自动创建 deleted_at 和 __restore_data 字段,数据删除时,备份唯一字段数据,并标记操作时间,查询时忽略已标记删除的数据

3 查询参数 QueryParam ​

在模型关联关系定义和调用处理器时,通过 Object QueryParam 描述查询条件。

json
{
  "select": ["id", "name", "mobile", "status"],
  "withs": {
    "manu": {
      "query": {
        "select": ["name", "short_name", "status"]
      }
    },
    "addresses": {}
  },
  "wheres": [
    { "column": "status", "value": "enabled" },
    { "rel": "manu", "column": "status", "value": "enabled" },
    {
      "wheres": [
        { "column": "name", "value": "%张三%", "op": "like" },
        {
          "method": "orwhere",
          "column": "name",
          "value": "%李四%",
          "op": "like"
        }
      ]
    }
  ],
  "orders": [
    { "column": "id", "option": "desc" },
    { "rel": "manu", "column": "name" }
  ],
  "limit": 2
}

应用引擎将以上查询条件解析为如下 SQL :

SQL
SELECT
  `user`.`id`,`user`.`name`,`user`.`mobile`,`user`.`status`,
  `user_manu`.`name` AS `user_manu_name`,
  `user_manu`.`short_name` AS `user_manu_short_name` ,
  `user_manu`.`status` AS `user_manu_status`
FROM `user` AS `user`
LEFT JOIN `manu` AS `user_manu` ON `user_manu`.`id` = `user`.`manu_id`
WHERE  `user`.`status` = 'enabled'
AND `user_manu`.`status` = 'enabled'
AND (
   `user`.`name` like '%张三%' OR `user`.`name` like '%李四%'
)
ORDER BY `user`.`id` desc, `user_manu`.`name` asc
LIMIT 2

3.1 数据结构 ​

QueryParam

字段类型说明必填项
selectArray<String>选择字段清单否
wheresArray<Object Where>查询条件否
ordersArray<Object Order>排序条件否
limitInteger返回记录条目否
pageInteger当前页码否
pagesizeInteger每页显示记录数量否
withs[key:String]:Object With读取关联模型否

Object Where

字段类型说明必填项
relString如按关联模型的字段查询,则填写关联模型名称否
columnString字段名称否
methodString查询方法 where,orwhere否
opString匹配关系 eq,like,in,gt 等否
valueAny匹配数值否
wheresArray<Object Where>分组查询否
查询方法说明
whereWHERE 字段 = 数值, WHERE 字段 >= 数值
orwhere... OR WHERE 字段 = 数值
匹配关系说明
eq默认值 等于 WHERE 字段 = 数值
like匹配 WHERE 字段 like 数值
gt大于 WHERE 字段 > 数值
ge大于等于 WHERE 字段 >= 数值
lt小于 WHERE 字段 < 数值
le小于等于 WHERE 字段 <= 数值
null为空 WHERE 字段 IS NULL
notnull不为空 WHERE 字段 IS NOT NULL
in列表包含 WHERE 字段 IN (数值...)

Object Order

字段类型说明必填项
relString如按关联模型的字段排序,则填写关联模型名称否
columnString字段名称否
optionString排序方式,默认为 asc desc, asc否

Object With

字段类型说明必填项
nameString关联关系名称否
queryObject QueryParam查询参数否

3.2 URL Query String 与 QueryParam 对照表 ​

查询条件可以通过 URL Query String 传入

4 处理器(process) ​

数据模型提供一组原子操作处理器 process , 这些处理器可用于服务接口(API)和数据流(Flow)编排。

处理器引用方式说明
findmodels.模型名称.Find查询单条记录
getmodels.模型名称.Get按条件查询, 不分页
paginatemodels.模型名称.Paginate按条件查询, 分页
createmodels.模型名称.Create创建单条记录, 返回新创建记录 ID
updatemodels.模型名称.Update更新单条记录
savemodels.模型名称.Save保存单条记录, 不存在创建记录, 存在更新记录, 返回记录 ID
deletemodels.模型名称.Delete删除单条记录(标记删除)
destroymodels.模型名称.Destroy删除单条记录(真删除)
insertmodels.模型名称.Insert插入多条记录, 返回插入行数
updatewheremodels.模型名称.UpdateWhere按条件更新记录, 返回更新行数
deletewheremodels.模型名称.DeleteWhere按条件删除数据, 返回删除行数(标记删除)
destroywheremodels.模型名称.DestroyWhere按条件删除数据, 返回删除行数(真删除)
eachsavemodels.模型名称.EachSave保存多条记录, 不存在创建记录, 存在更新记录, 返回记录 ID 集合
eachsaveAfterDeletemodels.模型名称.EachSaveAfterDelete删除一组给定 ID 的记录后,保存多条记录, 不存在创建记录, 存在更新记录, 返回记录 ID 集合

5. 完整示例 ​

完整示例保存在 examples 目录

json
{
  "name": "用户",
  "table": {
    "name": "user",
    "comment": "用户表",
    "engine": "InnoDB"
  },
  "columns": [
    { "label": "ID", "name": "id", "type": "ID" },
    {
      "label": "厂商",
      "name": "manu_id",
      "type": "bigInteger",
      "length": 50,
      "comment": "所属厂商",
      "nullable": true,
      "index": true,
      "validations": [
        {
          "method": "typeof",
          "args": ["integer"],
          "message": "{{input}}类型错误, {{label}}应为数字"
        },
        {
          "method": "min",
          "args": [0],
          "message": "{{label}}应大于0"
        }
      ]
    },
    {
      "label": "类型",
      "name": "type",
      "type": "enum",
      "option": ["admin", "staff", "user"],
      "comment": "账号类型 admin 管理员, staff 员工, user 用户",
      "default": "staff",
      "index": true,
      "validations": [
        {
          "method": "typeof",
          "args": ["string"],
          "message": "{{input}}类型错误, {{label}}应该为字符串"
        },
        {
          "method": "enum",
          "args": ["admin", "staff", "user"],
          "message": "{{input}}不在许可范围, {{label}}应该为 admin/staff/user"
        }
      ]
    },
    {
      "label": "手机号",
      "name": "mobile",
      "type": "string",
      "length": 50,
      "comment": "手机号",
      "index": true,
      "crypt": "AES",
      "validations": [
        {
          "method": "typeof",
          "args": ["string"],
          "message": "{{input}}类型错误, {{label}}应该为字符串"
        },
        {
          "method": "pattern",
          "args": ["^1[3-9]\\d{9}$"],
          "message": "{{input}}格式错误"
        }
      ]
    },
    {
      "label": "登录密码",
      "name": "password",
      "type": "string",
      "length": 256,
      "comment": "登录密码",
      "crypt": "PASSWORD",
      "index": true,
      "validations": [
        {
          "method": "typeof",
          "args": ["string"],
          "message": "{{input}}类型错误, {{label}}应该为字符串"
        },
        {
          "method": "minLength",
          "args": [6],
          "message": "{{label}}应该由6-18位,大小写字母、数字和符号构成"
        },
        {
          "method": "maxLength",
          "args": [18],
          "message": "{{label}}应该由6-18位,大小写字母、数字和符号构成"
        },
        {
          "method": "pattern",
          "args": ["[0-9]+"],
          "message": "{{label}}应该至少包含一个数字"
        },
        {
          "method": "pattern",
          "args": ["[A-Z]+"],
          "message": "{{label}}应该至少包含一个大写字母"
        },
        {
          "method": "pattern",
          "args": ["[a-z]+"],
          "message": "{{label}}应该至少包含一个小写字母"
        },
        {
          "method": "pattern",
          "args": ["[@#$&*]+"],
          "message": "{{label}}应该至少包含一个符号"
        }
      ]
    },
    {
      "label": "姓名",
      "name": "name",
      "type": "string",
      "length": 80,
      "comment": "姓名",
      "index": true,
      "validations": [
        {
          "method": "typeof",
          "args": ["string"],
          "message": "{{input}}类型错误, {{label}}应该为字符串"
        },
        {
          "method": "minLength",
          "args": [2],
          "message": "{{label}}至少需要2个字"
        },
        {
          "method": "maxLength",
          "args": [40],
          "message": "{{label}}不能超过20个字"
        }
      ]
    },
    {
      "label": "身份证号码",
      "name": "idcard",
      "type": "string",
      "length": 256,
      "comment": "身份证号码",
      "crypt": "AES",
      "nullable": true,
      "index": true,
      "validations": [
        {
          "method": "typeof",
          "args": ["string"],
          "message": "{{input}}类型错误, {{label}}应该为字符串"
        },
        {
          "method": "pattern",
          "args": ["^(\\d{18})|(\\d{14}X)$"],
          "message": "{{label}}格式错误"
        }
      ]
    },
    {
      "label": "账户余额",
      "name": "balance",
      "type": "integer",
      "length": 20,
      "comment": "账户余额(冗余)",
      "default": 0,
      "index": true,
      "validations": [
        {
          "method": "typeof",
          "args": ["integer"],
          "message": "{{input}}类型错误, {{label}}应为数字"
        },
        {
          "method": "min",
          "args": [0],
          "message": "{{label}}应大于0"
        }
      ]
    },
    {
      "label": "API Key",
      "name": "key",
      "type": "string",
      "length": 256,
      "comment": "API Key",
      "nullable": true,
      "unique": true,
      "validations": [
        {
          "method": "typeof",
          "args": ["string"],
          "message": "{{input}}类型错误, {{label}}应该为字符串"
        },
        {
          "method": "pattern",
          "args": ["^[0-9A-Za-z@#$&*]{8}$"],
          "message": " {{label}}应该由8位,大小写字母、数字和符号构成"
        }
      ]
    },
    {
      "label": "API 密钥",
      "name": "secret",
      "type": "string",
      "length": 256,
      "nullable": true,
      "crypt": "AES",
      "comment": "API 密钥",
      "index": true,
      "validations": [
        {
          "method": "typeof",
          "args": ["string"],
          "message": "{{input}}类型错误, {{label}}应该为字符串"
        },
        {
          "method": "pattern",
          "args": ["^[0-9A-Za-z@#$&*]{32}$"],
          "message": "{{label}}应该由32位,大小写字母、数字和符号构成"
        }
      ]
    },
    {
      "label": "简历",
      "name": "resume",
      "type": "text",
      "comment": "简历",
      "nullable": true
    },
    {
      "label": "扩展信息",
      "name": "extra",
      "type": "json",
      "comment": "扩展信息",
      "nullable": true
    },
    {
      "label": "状态",
      "comment": "用户状态 enabled 有效, disabled 无效",
      "name": "status",
      "type": "enum",
      "default": "enabled",
      "option": ["enabled", "disabled"],
      "index": true,
      "validations": [
        {
          "method": "typeof",
          "args": ["string"],
          "message": "{{input}}类型错误, {{label}}应该为字符串"
        },
        {
          "method": "enum",
          "args": ["enabled", "disabled"],
          "message": "{{input}}不在许可范围, {{label}}应该为 enabled/disabled"
        }
      ]
    }
  ],
  "relations": {
    "manu": {
      "type": "hasOne",
      "model": "manu",
      "key": "id",
      "foreign": "manu_id",
      "select": ["name", "short_name", "type"]
    },
    "addresses": {
      "type": "hasMany",
      "model": "address",
      "key": "user_id",
      "foreign": "id",
      "query": {
        "select": ["province", "city", "location", "status"],
        "pagesize": 20
      }
    },
    "mother": {
      "type": "hasOneThrough",
      "links": [
        {
          "type": "hasOne",
          "model": "friends",
          "key": "user_id",
          "foreign": "user.id",
          "query": {
            "select": ["status", "type", "friend_id"],
            "wheres": [
              {
                "column": "type",
                "value": "monther"
              }
            ]
          }
        },
        {
          "type": "hasOne",
          "model": "user",
          "key": "id",
          "foreign": "user_mother_friends.friend_id",
          "query": {
            "select": ["name", "id", "status", "type", "secret", "extra"],
            "withs": {
              "manu": { "name": "manu" },
              "roles": { "name": "roles" },
              "address": { "name": "address" }
            }
          }
        }
      ]
    },
    "roles": {
      "type": "hasManyThrough",
      "links": [
        {
          "type": "hasMany",
          "model": "user_roles",
          "key": "user_id",
          "foreign": "id",
          "query": {
            "select": ["status"],
            "pagesize": 20
          }
        },
        {
          "type": "hasOne",
          "model": "role",
          "key": "id",
          "foreign": "role_id",
          "query": {
            "select": ["name", "label", "permission"]
          }
        }
      ]
    }
  },
  "values": [
    {
      "name": "管理员",
      "manu_id": 1,
      "type": "admin",
      "idcard": "230624198301170015",
      "mobile": "13900001111",
      "password": "cvSK@RY6",
      "key": "FB3fxCeQ",
      "secret": "XMTdNRVigbgUiAPdiJCfaWgWcz2PaQXw",
      "status": "enabled",
      "extra": { "sex": "男" }
    },
    {
      "name": "员工",
      "manu_id": 1,
      "type": "staff",
      "idcard": "23082619820207024X",
      "mobile": "13900002222",
      "password": "qV@uT1DI",
      "key": "JDh2ZiUt",
      "secret": "wBeYjL7FjbcvpAdBrxtDFfjydsoPKhRN",
      "status": "enabled",
      "extra": { "sex": "女" }
    },
    {
      "name": "用户",
      "manu_id": 2,
      "type": "user",
      "idcard": "23082619820207004X",
      "mobile": "13900003333",
      "password": "qV@uT1DI",
      "key": "XZ12MiPz",
      "secret": "wBeYjL7FjbcvpAdBrxtDFfjydsoPKhRN",
      "status": "enabled",
      "extra": { "sex": "女" }
    }
  ],
  "indexes": [
    {
      "comment": "厂商用户",
      "name": "manu_id_mobile_unique",
      "columns": ["manu_id", "mobile"],
      "type": "unique"
    },
    {
      "comment": "简历全文检索",
      "name": "resume_fulltext",
      "columns": ["resume"],
      "type": "fulltext"
    }
  ],
  "option": { "timestamps": true, "soft_deletes": true }
}