+ 共享库支持

This commit is contained in:
Max 2021-11-06 15:11:53 +08:00
parent 30e57d6580
commit 42262ce799
17 changed files with 689 additions and 9 deletions

View file

@ -38,6 +38,7 @@ type XiangConfig struct {
RootModel string `json:"root_model,omitempty" env:"XIANG_ROOT_MODEL"` // 应用模型文件目录
RootFLow string `json:"root_flow,omitempty" env:"XIANG_ROOT_FLOW"` // 应用业务逻辑文件目录
RootPlugin string `json:"root_plugin,omitempty" env:"XIANG_ROOT_PLUGIN"` // 应用业务插件文件目录
RootLib string `json:"root_lib,omitempty" env:"XIANG_ROOT_LIB"` // 应用资料库文件目录
RootTable string `json:"root_table,omitempty" env:"XIANG_ROOT_TABLE"` // 应用数据表格文件目录
RootChart string `json:"root_chart,omitempty" env:"XIANG_ROOT_CHART"` // 应用分析图表文件目录
RootPage string `json:"root_page,omitempty" env:"XIANG_ROOT_PAGE"` // 应用通用页面文件目录
@ -148,6 +149,11 @@ func (cfg *Config) SetDefaults() {
if cfg.RootTable == "" {
cfg.RootTable = cfg.Root + "/tables"
}
if cfg.RootLib == "" {
cfg.RootLib = cfg.Root + "/libs"
}
if cfg.RootChart == "" {
cfg.RootChart = cfg.Root + "/charts"
}

View file

@ -28,6 +28,7 @@ func Load(cfg config.Config) {
LoadEngine(cfg.Path)
query.Load(cfg) // 加载数据分析引擎
share.Load(cfg) // 加载共享库 lib
model.Load(cfg) // 加载数据模型 model
api.Load(cfg) // 加载业务接口 API
flow.Load(cfg) // 加载业务逻辑 Flow

View file

@ -23,5 +23,5 @@ func check(t *testing.T) {
for key := range gou.Models {
keys = append(keys, key)
}
assert.Equal(t, 9, len(keys))
assert.Equal(t, 10, len(keys))
}

158
share/importable.go Normal file
View file

@ -0,0 +1,158 @@
package share
import (
"fmt"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/query/share"
"github.com/yaoapp/kun/exception"
"github.com/yaoapp/kun/maps"
"github.com/yaoapp/xiang/config"
)
// Libs 共享库
var Libs = map[string]map[string]interface{}{}
// Load 加载共享库
func Load(cfg config.Config) {
LoadFrom(cfg.RootLib)
}
// LoadFrom 从特定目录加载共享库
func LoadFrom(dir string) {
if DirNotExists(dir) {
return
}
Walk(dir, ".json", func(root, filename string) {
name := SpecName(root, filename)
content := ReadFile(filename)
libs := map[string]map[string]interface{}{}
err := jsoniter.Unmarshal(content, &libs)
if err != nil {
exception.New("共享数据结构异常 %s", 400, err).Throw()
}
for key, lib := range libs {
key := fmt.Sprintf("%s.%s", name, key)
Libs[key] = lib
// 删除注释
if _, has := lib["__comment"]; has {
delete(lib, "__comment")
}
}
})
}
// UnmarshalJSON Column 字段JSON解析
func (col *Column) UnmarshalJSON(data []byte) error {
new := ColumnImp{}
err := jsoniter.Unmarshal(data, &new)
if err != nil {
return err
}
// 导入
err = ImportJSON(new.Import, new.In, &new)
if err != nil {
return err
}
*col = Column(new)
return nil
}
// UnmarshalJSON Filter 字段JSON解析
func (filter *Filter) UnmarshalJSON(data []byte) error {
new := FilterImp{}
err := jsoniter.Unmarshal(data, &new)
if err != nil {
return err
}
// 导入
err = ImportJSON(new.Import, new.In, &new)
if err != nil {
return err
}
*filter = Filter(new)
return nil
}
// UnmarshalJSON Render 字段JSON解析
func (render *Render) UnmarshalJSON(data []byte) error {
new := RenderImp{}
err := jsoniter.Unmarshal(data, &new)
if err != nil {
return err
}
// 导入
err = ImportJSON(new.Import, new.In, &new)
if err != nil {
return err
}
*render = Render(new)
return nil
}
// UnmarshalJSON Page 字段JSON解析
func (page *Page) UnmarshalJSON(data []byte) error {
new := PageImp{}
err := jsoniter.Unmarshal(data, &new)
if err != nil {
return err
}
// 导入
err = ImportJSON(new.Import, new.In, &new)
if err != nil {
return err
}
*page = Page(new)
return nil
}
// UnmarshalJSON API 字段JSON解析
func (api *API) UnmarshalJSON(data []byte) error {
new := APIImp{}
err := jsoniter.Unmarshal(data, &new)
if err != nil {
return err
}
// 导入
err = ImportJSON(new.Import, new.In, &new)
if err != nil {
return err
}
*api = API(new)
return nil
}
// ImportJSON 导入
func ImportJSON(name string, in []interface{}, v interface{}) error {
if name == "" {
return nil
}
lib, has := Libs[name]
if !has {
return fmt.Errorf("共享库 %s 不存在", name)
}
data := maps.MapStrAny{"$in": in}.Dot()
content, err := jsoniter.Marshal(share.Bind(lib, data))
if err != nil {
return err
}
err = jsoniter.Unmarshal(content, v)
if err != nil {
return err
}
return nil
}

57
share/importable_test.go Normal file
View file

@ -0,0 +1,57 @@
package share
import (
"path"
"testing"
jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/xiang/config"
)
func init() {
rootLib := path.Join(config.Conf.Source, "/tests/libs")
LoadFrom(rootLib)
}
func TestColumn(t *testing.T) {
content := `{ "@": "column.Image", "in": ["LOGO", ":logo", 40] }`
column := Column{}
jsoniter.Unmarshal([]byte(content), &column)
assert.Equal(t, "upload", column.Edit.Type)
assert.Equal(t, ":logo", column.Edit.Props["value"])
assert.Equal(t, "image", column.View.Type)
assert.Equal(t, float64(40), column.View.Props["height"])
assert.Equal(t, float64(40), column.View.Props["width"])
assert.Equal(t, ":logo", column.View.Props["value"])
}
func TestFilter(t *testing.T) {
content := `{ "@": "filter.关键词", "in": ["where.name.match"] }`
filter := Filter{}
jsoniter.Unmarshal([]byte(content), &filter)
assert.Equal(t, "where.name.match", filter.Bind)
}
func TestRender(t *testing.T) {
content := `{ "@": "render.Image", "in": [":image", 40, 60] }`
render := Render{}
jsoniter.Unmarshal([]byte(content), &render)
assert.Equal(t, ":image", render.Props["value"])
assert.Equal(t, float64(40), render.Props["width"])
assert.Equal(t, float64(60), render.Props["height"])
}
func TestPage(t *testing.T) {
content := `{ "@": "pages.static.Page", "in": ["id"] }`
page := Page{}
jsoniter.Unmarshal([]byte(content), &page)
assert.Equal(t, "id", page.Primary)
}
func TestAPI(t *testing.T) {
content := `{ "@": "apis.table.Search", "in": [10] }`
api := API{}
jsoniter.Unmarshal([]byte(content), &api)
assert.Equal(t, []interface{}{nil, nil, float64(10)}, api.Default)
}

View file

@ -2,6 +2,15 @@ package share
import "github.com/yaoapp/kun/maps"
// Importable 可导入JSON
type Importable struct {
Import string `json:"@,omitempty"` // 从 Global 或 Vendor 载入
In []interface{} `json:"in,omitempty"` // 从 Global 或 Vendor 载入, 解析参数
}
// APIImp 导入配置数据结构
type APIImp API
// API API 配置数据结构
type API struct {
Name string `json:"-"`
@ -10,36 +19,53 @@ type API struct {
Process string `json:"process,omitempty"`
Guard string `json:"guard,omitempty"`
Default []interface{} `json:"default,omitempty"`
Importable
}
// ColumnImp 导入模式查询过滤器
type ColumnImp Column
// Column 字段呈现方式
type Column struct {
Label string `json:"label"`
View Render `json:"view,omitempty"`
Edit Render `json:"edit,omitempty"`
Form Render `json:"form,omitempty"`
Importable
}
// FilterImp 导入模式查询过滤器
type FilterImp Filter
// Filter 查询过滤器
type Filter struct {
Label string `json:"label"`
Bind string `json:"bind,omitempty"`
Input Render `json:"input,omitempty"`
Importable
}
// RenderImp 导入模式组件渲染方式
type RenderImp Render
// Render 组件渲染方式
type Render struct {
Type string `json:"type,omitempty"`
Props map[string]interface{} `json:"props,omitempty"`
Components map[string]interface{} `json:"components,omitempty"`
Importable
}
// PageImp 导入模式页面
type PageImp Page
// Page 页面
type Page struct {
Primary string `json:"primary"`
Layout map[string]interface{} `json:"layout"`
Actions map[string]Render `json:"actions,omitempty"`
Option map[string]interface{} `json:"option,omitempty"`
}
// Render 组件渲染方式
type Render struct {
Type string `json:"type,omitempty"`
Props map[string]interface{} `json:"props,omitempty"`
Components map[string]interface{} `json:"components,omitempty"`
Importable
}
// AppInfo 应用信息

View file

@ -16,6 +16,7 @@ import (
func init() {
share.DBConnect(config.Conf.Database)
model.Load(config.Conf)
share.Load(config.Conf)
Load(config.Conf)
}
func TestTableProcessSearch(t *testing.T) {

View file

@ -12,6 +12,7 @@ import (
func TestLoad(t *testing.T) {
share.DBConnect(config.Conf.Database)
share.Load(config.Conf)
model.Load(config.Conf)
Tables = make(map[string]*Table)
@ -47,5 +48,5 @@ func check(t *testing.T) {
for key := range Tables {
keys = append(keys, key)
}
assert.Equal(t, 2, len(keys))
assert.Equal(t, 3, len(keys))
}

14
tests/libs/action.json Normal file
View file

@ -0,0 +1,14 @@
{
"保存": {
"type": "button",
"props": {
"label": "保存"
}
},
"删除": {
"type": "button",
"props": {
"label": "保存"
}
}
}

View file

@ -0,0 +1,5 @@
{
"Search": {
"default": [null, null, "{{$in.0}}"]
}
}

32
tests/libs/column.json Normal file
View file

@ -0,0 +1,32 @@
{
"Label": {
"__comment": "column.Lable('ID',':id')",
"label": "{{$in.0}}",
"view": {
"props": {
"value": "{{$in.1}}"
}
}
},
"Image": {
"__comment": "column.Image('图标',':icon', 40)",
"label": "{{$in.0}}",
"view": {
"type": "image",
"props": {
"value": "{{$in.1}}",
"width": "{{$in.2}}",
"height": "{{$in.2}}"
}
},
"edit": {
"type": "upload",
"props": {
"value": "{{$in.1}}",
"filetype": "image",
"multiple": false,
"maxCount": 1
}
}
}
}

47
tests/libs/filter.json Normal file
View file

@ -0,0 +1,47 @@
{
"关键词": {
"__comment": "filter.关键词('where.name.match')",
"label": "关键词",
"bind": "{{$in.0}}",
"input": {
"type": "input",
"props": {
"placeholder": "请输入关键词"
}
}
},
"排序": {
"__comment": "filter.排序()",
"label": "排序方式",
"bind": "order",
"input": {
"type": "select",
"props": {
"placeholder": "排序方式",
"options": [
{ "label": "手动顺序", "value": "rank" },
{ "label": "最近更新", "value": "updated_at.desc" },
{ "label": "最近创建", "value": "created_at.desc" }
]
}
}
},
"客户状态": {
"__comment": "filter.客户状态()",
"label": "状态",
"bind": "where.status.in",
"input": {
"type": "select",
"props": {
"placeholder": "选择厂商状态",
"mode": "multiple",
"options": [
{ "value": "已注册" },
{ "value": "待收录" },
{ "value": "待审核" },
{ "value": "已禁用" }
]
}
}
}
}

View file

@ -0,0 +1,16 @@
{
"Page": {
"primary": "{{$in.0}}",
"layout": {
"filters": [
{ "name": "开始时间", "width": 6 },
{ "name": "结束时间", "width": 6 }
],
"charts": [
{ "type": "line", "props": {} },
{ "type": "bar", "props": {} }
]
},
"actions": {}
}
}

10
tests/libs/render.json Normal file
View file

@ -0,0 +1,10 @@
{
"Image": {
"type": "image",
"props": {
"value": "{{$in.0}}",
"width": "{{$in.1}}",
"height": "{{$in.2}}"
}
}
}

View file

@ -0,0 +1,230 @@
{
"name": "客户",
"table": {
"name": "customer",
"comment": "客户表",
"engine": "InnoDB"
},
"columns": [
{
"label": "ID",
"name": "id",
"type": "ID",
"comment": "ID"
},
{
"label": "来源",
"name": "channel_id",
"type": "bigInteger",
"comment": "客户来源"
},
{
"label": "公司名称",
"name": "name",
"type": "string",
"length": 200,
"comment": "公司全称",
"unique": true
},
{
"label": "公司简称",
"name": "short_name",
"type": "string",
"length": 200,
"comment": "简称",
"nullable": true,
"index": true
},
{
"name": "credit_no",
"type": "string",
"length": 200,
"comment": "统一社会信用代码",
"nullable": true,
"unique": true
},
{
"name": "oper_name",
"type": "string",
"length": 50,
"comment": "法人代表",
"nullable": true,
"index": true
},
{
"label": "注册资本(万元)",
"name": "reg_capi",
"type": "integer",
"comment": "注册资本(万元)",
"nullable": true,
"index": true
},
{
"label": "注册时间",
"name": "opened_at",
"type": "date",
"comment": "注册时间",
"nullable": true,
"index": true
},
{
"label": "省份",
"name": "province",
"type": "string",
"length": 50,
"comment": "总部所在省份",
"nullable": true,
"index": true
},
{
"label": "城市",
"name": "city",
"type": "string",
"length": 100,
"comment": "总部所在城市",
"nullable": true,
"index": true
},
{
"label": "地址",
"name": "address",
"type": "string",
"length": 255,
"comment": "总部地址",
"nullable": true,
"index": true
},
{
"label": "经度",
"name": "lng",
"type": "unsignedDecimal",
"precision": 12,
"scale": 6,
"comment": "经度",
"nullable": true,
"index": true
},
{
"label": "纬度",
"name": "lat",
"type": "unsignedDecimal",
"comment": "纬度",
"precision": 12,
"scale": 6,
"nullable": true,
"index": true
},
{
"label": "经营状态",
"name": "company_status",
"type": "string",
"length": 255,
"comment": "经营状态",
"nullable": true,
"index": true
},
{
"label": "LOGO",
"name": "logo",
"type": "json",
"comment": "LOGO",
"nullable": true
},
{
"label": "简介",
"name": "summary",
"type": "string",
"length": 600,
"comment": "简介",
"nullable": true,
"index": true
},
{
"label": "官网",
"name": "link",
"type": "string",
"length": 200,
"comment": "官网地址",
"nullable": true
},
{
"label": "联系人",
"name": "contact_name",
"type": "string",
"length": 200,
"comment": "联系人姓名",
"nullable": true,
"index": true
},
{
"label": "联系人职务",
"name": "contact_title",
"type": "string",
"length": 200,
"comment": "联系人职务",
"nullable": true,
"index": true
},
{
"label": "联系电话",
"name": "contact_mobile",
"type": "string",
"comment": "联系电话",
"nullable": true,
"index": true
},
{
"label": "联系微信",
"name": "contact_wechat",
"type": "string",
"comment": "联系微信",
"nullable": true,
"index": true
},
{
"label": "介绍",
"name": "desc",
"type": "text",
"comment": "介绍",
"nullable": true
},
{
"label": "领域",
"name": "fields",
"type": "json",
"comment": "服务领域(多选)",
"nullable": true,
"index": true
},
{
"label": "行业",
"name": "industries",
"type": "json",
"comment": "行业覆盖(多选)",
"nullable": true,
"index": true
},
{
"label": "标签",
"name": "tags",
"type": "json",
"comment": "标签(多选)",
"nullable": true,
"index": true
},
{
"label": "关系",
"name": "relation",
"type": "enum",
"default": "正在接洽",
"option": ["已有合作", "正在接洽", "潜在客户"],
"comment": "合作关系: 已有合作, 正在接洽, 潜在客户",
"index": true
}
],
"relations": {},
"option": {
"timestamps": true,
"soft_deletes": true
}
}

View file

@ -27,6 +27,7 @@
"type": "string",
"length": 50,
"comment": "城市",
"nullable": true,
"index": true
},
{

View file

@ -0,0 +1,75 @@
{
"name": "客户",
"version": "1.0.0",
"decription": "客户管理数据表格",
"bind": {
"model": "user"
},
"apis": {
"search": {}
},
"columns": {
"LOGO": { "@": "column.Image", "in": ["LOGO", ":logo", 40] },
"ID": { "@": "column.Label", "in": ["ID", ":id"] }
},
"filters": {
"关键词": { "@": "filter.关键词", "in": ["where.name.match"] },
"排序": { "@": "filter.排序" },
"状态": { "@": "filter.客户状态" }
},
"list": {
"primary": "id",
"layout": {
"columns": [
{ "name": "ID", "width": 80 },
{ "name": "LOGO", "width": 300 }
],
"filters": [
{ "name": "关键词" },
{ "name": "状态", "width": 3 },
{ "name": "排序", "width": 3 }
]
},
"actions": {
"create": {
"type": "button",
"props": {
"label": "添加厂商",
"icon": "fas fa-plus"
}
},
"pagination": {
"props": { "showTotal": true }
}
},
"option": {
"batch": {
"delete": {},
"columns": [
{ "name": "状态", "width": 12 },
{ "name": "类型", "width": 12 },
{ "name": "排序", "width": 12 }
]
}
}
},
"edit": {
"primary": "id",
"layout": {
"fieldset": [
{
"title": "客户资料",
"description": "",
"columns": [{ "name": "LOGO", "width": 12 }]
}
]
},
"actions": {
"cancel": {},
"save": { "@": "action.保存" },
"delete": { "@": "action.删除" }
}
},
"insert": {},
"view": {}
}