+ Chart 处理器
This commit is contained in:
parent
afd781cd86
commit
2a457033cb
13 changed files with 520 additions and 30 deletions
60
chart/api.go
Normal file
60
chart/api.go
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yaoapp/xiang/share"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupAPIs 设定API数据
|
||||||
|
func (chart *Chart) SetupAPIs() {
|
||||||
|
|
||||||
|
defaults := map[string]share.API{
|
||||||
|
"data": apiDataDefault(),
|
||||||
|
"setting": apiSettingDefault(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开发者填写的规则
|
||||||
|
for name := range chart.APIs {
|
||||||
|
if _, has := defaults[name]; !has {
|
||||||
|
delete(chart.APIs, name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
api := defaults[name]
|
||||||
|
api.Name = name
|
||||||
|
if chart.APIs[name].Process != "" {
|
||||||
|
api.Process = chart.APIs[name].Process
|
||||||
|
}
|
||||||
|
|
||||||
|
if chart.APIs[name].Guard != "" {
|
||||||
|
api.Guard = chart.APIs[name].Guard
|
||||||
|
}
|
||||||
|
|
||||||
|
if chart.APIs[name].Default != nil {
|
||||||
|
api.Default = chart.APIs[name].Default
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults[name] = api
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.APIs = defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiSearchDefault data 接口默认值
|
||||||
|
func apiDataDefault() share.API {
|
||||||
|
param := map[string]interface{}{}
|
||||||
|
return share.API{
|
||||||
|
Name: "data",
|
||||||
|
Guard: "bearer-jwt",
|
||||||
|
Process: "xiang.chart.data",
|
||||||
|
Default: []interface{}{param},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiSettingDefault setting 接口默认值
|
||||||
|
func apiSettingDefault() share.API {
|
||||||
|
return share.API{
|
||||||
|
Name: "setting",
|
||||||
|
Guard: "bearer-jwt",
|
||||||
|
Process: "xiang.chart.setting",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package chart
|
package chart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"github.com/yaoapp/gou"
|
||||||
"github.com/yaoapp/kun/exception"
|
"github.com/yaoapp/kun/exception"
|
||||||
"github.com/yaoapp/xiang/config"
|
"github.com/yaoapp/xiang/config"
|
||||||
"github.com/yaoapp/xiang/share"
|
"github.com/yaoapp/xiang/share"
|
||||||
|
|
@ -36,7 +39,11 @@ func LoadFrom(dir string, prefix string) {
|
||||||
|
|
||||||
// LoadChart 载入数据表格
|
// LoadChart 载入数据表格
|
||||||
func LoadChart(source []byte, name string) (*Chart, error) {
|
func LoadChart(source []byte, name string) (*Chart, error) {
|
||||||
chart := Chart{}
|
chart := Chart{
|
||||||
|
Flow: gou.Flow{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
}
|
||||||
err := jsoniter.Unmarshal(source, &chart)
|
err := jsoniter.Unmarshal(source, &chart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Println(name)
|
xlog.Println(name)
|
||||||
|
|
@ -45,5 +52,24 @@ func LoadChart(source []byte, name string) (*Chart, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
chart.Prepare()
|
chart.Prepare()
|
||||||
|
chart.SetupAPIs()
|
||||||
|
|
||||||
return &chart, nil
|
return &chart, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Select 读取已加载图表
|
||||||
|
func Select(name string) *Chart {
|
||||||
|
chart, has := Charts[name]
|
||||||
|
if !has {
|
||||||
|
exception.New(
|
||||||
|
fmt.Sprintf("Chart:%s; 尚未加载", name),
|
||||||
|
400,
|
||||||
|
).Throw()
|
||||||
|
}
|
||||||
|
return chart
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetData 运行 flow 返回数值
|
||||||
|
func (chart Chart) GetData(params map[string]interface{}) interface{} {
|
||||||
|
return chart.Flow.Exec(params)
|
||||||
|
}
|
||||||
|
|
|
||||||
82
chart/process.go
Normal file
82
chart/process.go
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou"
|
||||||
|
"github.com/yaoapp/kun/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 注册处理器
|
||||||
|
func init() {
|
||||||
|
gou.RegisterProcessHandler("xiang.chart.data", ProcessData)
|
||||||
|
gou.RegisterProcessHandler("xiang.chart.setting", ProcessSetting)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessData xiang.chart.data
|
||||||
|
// 查询数据分析图表中定义的数据
|
||||||
|
func ProcessData(process *gou.Process) interface{} {
|
||||||
|
|
||||||
|
process.ValidateArgNums(3)
|
||||||
|
name := process.ArgsString(0)
|
||||||
|
params := process.ArgsMap(1)
|
||||||
|
chart := Select(name)
|
||||||
|
api := chart.APIs["data"]
|
||||||
|
if process.NumOfArgsIs(4) && api.IsAllow(process.Args[3]) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(api.Default) > 0 {
|
||||||
|
if defaults, ok := api.Default[0].(map[string]interface{}); ok {
|
||||||
|
for key, value := range defaults {
|
||||||
|
if !params.Has(key) {
|
||||||
|
params.Set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chart.GetData(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessSetting xiang.chart.setting
|
||||||
|
// 查询数据分析图表中定义的数据
|
||||||
|
func ProcessSetting(process *gou.Process) interface{} {
|
||||||
|
|
||||||
|
process.ValidateArgNums(2)
|
||||||
|
name := process.ArgsString(0)
|
||||||
|
field := process.ArgsString(1)
|
||||||
|
chart := Select(name)
|
||||||
|
api := chart.APIs["setting"]
|
||||||
|
if process.NumOfArgsIs(2) && api.IsAllow(process.Args[1]) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.Split(field, ",")
|
||||||
|
setting := maps.Map{
|
||||||
|
"name": chart.Name,
|
||||||
|
"label": chart.Label,
|
||||||
|
"version": chart.Version,
|
||||||
|
"description": chart.Description,
|
||||||
|
"filters": chart.Filters,
|
||||||
|
"page": chart.Page,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fields) == 1 && setting.Has(fields[0]) {
|
||||||
|
field := strings.TrimSpace(fields[0])
|
||||||
|
return setting.Get(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fields) > 1 {
|
||||||
|
res := maps.Map{}
|
||||||
|
for _, field := range fields {
|
||||||
|
field = strings.TrimSpace(field)
|
||||||
|
if setting.Has(field) {
|
||||||
|
res.Set(field, setting.Get(field))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return setting
|
||||||
|
|
||||||
|
}
|
||||||
77
chart/process_test.go
Normal file
77
chart/process_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/yaoapp/gou"
|
||||||
|
"github.com/yaoapp/kun/any"
|
||||||
|
"github.com/yaoapp/kun/utils"
|
||||||
|
"github.com/yaoapp/xiang/config"
|
||||||
|
_ "github.com/yaoapp/xiang/helper"
|
||||||
|
"github.com/yaoapp/xiang/model"
|
||||||
|
"github.com/yaoapp/xiang/query"
|
||||||
|
"github.com/yaoapp/xiang/share"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
share.DBConnect(config.Conf.Database)
|
||||||
|
model.Load(config.Conf)
|
||||||
|
query.Load(config.Conf)
|
||||||
|
Load(config.Conf)
|
||||||
|
}
|
||||||
|
func TestProcessSetting(t *testing.T) {
|
||||||
|
|
||||||
|
args := []interface{}{
|
||||||
|
"service.compare",
|
||||||
|
nil,
|
||||||
|
&gin.Context{},
|
||||||
|
}
|
||||||
|
process := gou.NewProcess("xiang.chart.Setting", args...)
|
||||||
|
response := ProcessSetting(process)
|
||||||
|
assert.NotNil(t, response)
|
||||||
|
res := any.Of(response).Map()
|
||||||
|
assert.True(t, res.Has("name"))
|
||||||
|
assert.True(t, res.Has("label"))
|
||||||
|
assert.True(t, res.Has("description"))
|
||||||
|
assert.True(t, res.Has("page"))
|
||||||
|
assert.True(t, res.Has("version"))
|
||||||
|
|
||||||
|
args = []interface{}{
|
||||||
|
"service.compare",
|
||||||
|
"page,name",
|
||||||
|
&gin.Context{},
|
||||||
|
}
|
||||||
|
process = gou.NewProcess("xiang.chart.Setting", args...)
|
||||||
|
response = ProcessSetting(process)
|
||||||
|
assert.NotNil(t, response)
|
||||||
|
|
||||||
|
res = any.Of(response).Map()
|
||||||
|
assert.True(t, res.Has("name"))
|
||||||
|
assert.True(t, res.Has("page"))
|
||||||
|
assert.False(t, res.Has("label"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessData(t *testing.T) {
|
||||||
|
|
||||||
|
params := url.Values{
|
||||||
|
"from": []string{"1981-01-01", "1990-01-01"},
|
||||||
|
}
|
||||||
|
params.Set("to", "2049-12-31")
|
||||||
|
|
||||||
|
args := []interface{}{
|
||||||
|
"service.compare",
|
||||||
|
params,
|
||||||
|
&gin.Context{},
|
||||||
|
}
|
||||||
|
process := gou.NewProcess("xiang.chart.Data", args...)
|
||||||
|
response := ProcessData(process)
|
||||||
|
utils.Dump(response)
|
||||||
|
|
||||||
|
assert.NotNil(t, response)
|
||||||
|
|
||||||
|
res := any.Of(response).Map().Dot()
|
||||||
|
assert.Equal(t, "北京", res.Get("合并.0.城市"))
|
||||||
|
}
|
||||||
|
|
@ -8,14 +8,7 @@ import (
|
||||||
// Chart 图表格式
|
// Chart 图表格式
|
||||||
type Chart struct {
|
type Chart struct {
|
||||||
gou.Flow
|
gou.Flow
|
||||||
APIs map[string]API `json:"apis,omitempty"`
|
APIs map[string]share.API `json:"apis,omitempty"`
|
||||||
Filters map[string]share.Filter `json:"filters,omitempty"`
|
Filters map[string]share.Filter `json:"filters,omitempty"`
|
||||||
Page share.Page `json:"page,omitempty"`
|
Page share.Page `json:"page,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 图表 API
|
|
||||||
type API struct {
|
|
||||||
Disable bool `json:"disable,omitempty"`
|
|
||||||
Guard string `json:"guard,omitempty"`
|
|
||||||
Default interface{} `json:"default,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
|
||||||
78
helper/array.go
Normal file
78
helper/array.go
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"github.com/yaoapp/kun/exception"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ArrayPluckValue ArrayPluck 参数
|
||||||
|
type ArrayPluckValue struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Items []map[string]interface{} `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArrayPluck 将多个数据记录集合,合并为一个数据记录集合
|
||||||
|
// columns: ["城市", "行业", "计费"]
|
||||||
|
// pluck: {
|
||||||
|
// "行业":{"key":"city", "value":"数量", "items":[{"city":"北京", "数量":32},{"city":"上海", "数量":20}]},
|
||||||
|
// "计费":{"key":"city", "value":"计费种类", "items":[{"city":"北京", "计费种类":6},{"city":"西安", "计费种类":3}]},
|
||||||
|
// }
|
||||||
|
// return: [
|
||||||
|
// {"城市":"北京", "行业":32, "计费":6},
|
||||||
|
// {"城市":"上海", "行业":20, "计费":null},
|
||||||
|
// {"城市":"西安", "行业":null, "计费":6}
|
||||||
|
// ]
|
||||||
|
func ArrayPluck(columns []string, pluck map[string]interface{}) []map[string]interface{} {
|
||||||
|
if len(columns) < 2 {
|
||||||
|
exception.New("ArrayPluck 参数错误, 应至少包含两列。", 400).Ctx(columns).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
primary := columns[0]
|
||||||
|
data := map[string]map[string]interface{}{}
|
||||||
|
|
||||||
|
// 解析数据
|
||||||
|
for name, value := range pluck { // name="行业", value={"key":"city", "value":"数量", "items":[{"city":"北京", "数量":32},{"city":"上海", "数量":20}]},
|
||||||
|
arg := OfArrayPluckValue(value)
|
||||||
|
for _, item := range arg.Items { // item = [{"city":"北京", "数量":32},{"city":"上海", "数量":20}]
|
||||||
|
if v, has := item[arg.Key]; has { // arg.Key = "city"
|
||||||
|
key := fmt.Sprintf("%#v", v) // key = `"北京"`
|
||||||
|
val := item[arg.Value] // arg.Value = "数量", val = 32
|
||||||
|
if _, has := data[key]; !has {
|
||||||
|
data[key] = map[string]interface{}{} // {`"北京"`: {}}
|
||||||
|
data[key][primary] = v // {`"北京"`: {"城市":"北京"}}
|
||||||
|
}
|
||||||
|
data[key][name] = val // {`"北京"`: {"城市":"北京", "行业":32}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空值处理
|
||||||
|
res := []map[string]interface{}{}
|
||||||
|
for key := range data { // key = `"北京"`
|
||||||
|
for name := range pluck { // name = "行业"
|
||||||
|
if _, has := data[key][name]; !has {
|
||||||
|
data[key][name] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res = append(res, data[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// OfArrayPluckValue Any 转 ArrayPluckValue
|
||||||
|
func OfArrayPluckValue(any interface{}) ArrayPluckValue {
|
||||||
|
content, err := jsoniter.Marshal(any)
|
||||||
|
if err != nil {
|
||||||
|
exception.New("ArrayPluck 参数错误", 400).Ctx(err.Error()).Throw()
|
||||||
|
}
|
||||||
|
value := ArrayPluckValue{Items: []map[string]interface{}{}}
|
||||||
|
err = jsoniter.Unmarshal(content, &value)
|
||||||
|
if err != nil {
|
||||||
|
exception.New("ArrayPluck 参数错误", 400).Ctx(err.Error()).Throw()
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
23
helper/array_test.go
Normal file
23
helper/array_test.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/yaoapp/kun/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArrayPluck(t *testing.T) {
|
||||||
|
columns := []string{"城市", "行业", "计费"}
|
||||||
|
pluck := map[string]interface{}{
|
||||||
|
"行业": map[string]interface{}{"key": "city", "value": "数量", "items": []map[string]interface{}{{"city": "北京", "数量": 32}, {"city": "上海", "数量": 20}}},
|
||||||
|
"计费": map[string]interface{}{"key": "city", "value": "计费种类", "items": []map[string]interface{}{{"city": "北京", "计费种类": 6}, {"city": "西安", "计费种类": 3}}},
|
||||||
|
}
|
||||||
|
items := ArrayPluck(columns, pluck)
|
||||||
|
assert.Equal(t, 3, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
maps.Of(item).Has("城市")
|
||||||
|
maps.Of(item).Has("行业")
|
||||||
|
maps.Of(item).Has("计费")
|
||||||
|
}
|
||||||
|
}
|
||||||
36
helper/process.go
Normal file
36
helper/process.go
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yaoapp/gou"
|
||||||
|
"github.com/yaoapp/kun/exception"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// 注册处理器
|
||||||
|
gou.RegisterProcessHandler("xiang.helper.ArrayPluck", ProcessArrayPluck)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessArrayPluck xiang.helper.ArrayPluck 将多个数据记录集合,合并为一个数据记录集合
|
||||||
|
func ProcessArrayPluck(process *gou.Process) interface{} {
|
||||||
|
process.ValidateArgNums(2)
|
||||||
|
columnsAny := process.Args[0]
|
||||||
|
columns := []string{}
|
||||||
|
switch columnsAny.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
for _, v := range columnsAny.([]interface{}) {
|
||||||
|
value, ok := v.(string)
|
||||||
|
if ok {
|
||||||
|
columns = append(columns, value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
exception.New("参数错误: 第1个参数不是字符串数组", 400).Ctx(process.Args[0]).Throw()
|
||||||
|
}
|
||||||
|
case []string:
|
||||||
|
default:
|
||||||
|
exception.New("参数错误: 第1个参数不是字符串数组", 400).Ctx(process.Args[0]).Throw()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pluck := process.ArgsMap(1)
|
||||||
|
return ArrayPluck(columns, pluck)
|
||||||
|
}
|
||||||
31
helper/process_test.go
Normal file
31
helper/process_test.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/yaoapp/gou"
|
||||||
|
"github.com/yaoapp/kun/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProcessArrayPluck(t *testing.T) {
|
||||||
|
args := []interface{}{
|
||||||
|
[]interface{}{"城市", "行业", "计费"},
|
||||||
|
map[string]interface{}{
|
||||||
|
"行业": map[string]interface{}{"key": "city", "value": "数量", "items": []map[string]interface{}{{"city": "北京", "数量": 32}, {"city": "上海", "数量": 20}}},
|
||||||
|
"计费": map[string]interface{}{"key": "city", "value": "计费种类", "items": []map[string]interface{}{{"city": "北京", "计费种类": 6}, {"city": "西安", "计费种类": 3}}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
process := gou.NewProcess("xiang.helper.ArrayPluck", args...)
|
||||||
|
response := ProcessArrayPluck(process)
|
||||||
|
assert.NotNil(t, response)
|
||||||
|
items, ok := response.([]map[string]interface{})
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
assert.Equal(t, 3, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
maps.Of(item).Has("城市")
|
||||||
|
maps.Of(item).Has("行业")
|
||||||
|
maps.Of(item).Has("计费")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import "github.com/yaoapp/kun/maps"
|
||||||
type API struct {
|
type API struct {
|
||||||
Name string `json:"-"`
|
Name string `json:"-"`
|
||||||
Source string `json:"-"`
|
Source string `json:"-"`
|
||||||
|
Disable bool `json:"disable,omitempty"`
|
||||||
Process string `json:"process,omitempty"`
|
Process string `json:"process,omitempty"`
|
||||||
Guard string `json:"guard,omitempty"`
|
Guard string `json:"guard,omitempty"`
|
||||||
Default []interface{} `json:"default,omitempty"`
|
Default []interface{} `json:"default,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,92 @@
|
||||||
{
|
{
|
||||||
"engine": "gou",
|
|
||||||
"label": "指标对比",
|
"label": "指标对比",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"decription": "指标对比",
|
"description": "指标对比用于测试",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"name": "行业分布",
|
"name": "行业",
|
||||||
"engine": "xiang",
|
"engine": "xiang",
|
||||||
"query": {
|
"query": {
|
||||||
"select": ["city", ":COUNT(id) as cnt", "industries[*] as industry"],
|
"debug": true,
|
||||||
|
"select": [
|
||||||
|
"city",
|
||||||
|
":COUNT(id) as 数量",
|
||||||
|
"industries[*](string 50) as industry"
|
||||||
|
],
|
||||||
"from": "$service",
|
"from": "$service",
|
||||||
"wheres": [
|
"wheres": [
|
||||||
{ "field": "created_at", ">": "?:$in.from" },
|
{ "field": "created_at", ">": "?:$from.0" },
|
||||||
{ "field": "created_at", "<": "?:$in.to" }
|
{ "field": "created_at", "<": "?:$to" }
|
||||||
],
|
],
|
||||||
"order": "cnt desc",
|
"orders": "数量 desc",
|
||||||
"limit": 100,
|
"limit": 100
|
||||||
"group": ["industries[*]", "city"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "计费方式",
|
"name": "计费",
|
||||||
"engine": "xiang",
|
"engine": "xiang",
|
||||||
"query": {
|
"query": {
|
||||||
"select": ["city", ":COUNT(id) as cnt", "price_options[*] as option"],
|
"select": ["city", ":COUNT(id) as 数量", "price_options[*] as option"],
|
||||||
"from": "$service",
|
"from": "$service",
|
||||||
"wheres": [
|
"wheres": [
|
||||||
{ "field": "created_at", ">": "?:$in.from" },
|
{ "field": "created_at", ">": "?:$from.0" },
|
||||||
{ "field": "created_at", "<": "?:$in.to" }
|
{ "field": "created_at", "<": "?:$to" }
|
||||||
],
|
],
|
||||||
"order": "cnt desc",
|
"orders": "数量 desc",
|
||||||
"limit": 100,
|
"limit": 100
|
||||||
"group": ["price_options[*]", "city"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "合并结果",
|
"name": "合并结果",
|
||||||
"process": "xiang.chart.MergeData",
|
"process": "xiang.helper.ArrayPluck",
|
||||||
"args": [
|
"args": [
|
||||||
["城市", "行业", "计费"],
|
["城市", "行业", "计费"],
|
||||||
{
|
{
|
||||||
"行业": { "key": "city", "values": "{{$res.行业分布[*].industry}}" },
|
"行业": { "key": "city", "value": "数量", "items": "{{$res.行业}}" },
|
||||||
"计费": { "key": "city", "values": "{{$res.计费方式[*].option}}" }
|
"计费": { "key": "city", "value": "数量", "items": "{{$res.计费}}" }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"output": "{{$res.合并结果}}",
|
"output": {
|
||||||
"filters": {},
|
"合并": "{{$res.合并结果}}",
|
||||||
|
"计费": "{{$res.计费}}",
|
||||||
|
"行业": "{{$res.行业}}",
|
||||||
|
"参数": "{{$in}}"
|
||||||
|
},
|
||||||
|
"apis": {
|
||||||
|
"data": {
|
||||||
|
"disable": false,
|
||||||
|
"guard": "-",
|
||||||
|
"default": [{ "from": "1980-01-02", "to": "2050-12-31" }]
|
||||||
|
},
|
||||||
|
"setting": {
|
||||||
|
"disable": false,
|
||||||
|
"guard": "-"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"开始时间": {
|
||||||
|
"label": "开始时间",
|
||||||
|
"bind": "from",
|
||||||
|
"input": {
|
||||||
|
"type": "date",
|
||||||
|
"props": {
|
||||||
|
"placeholder": "请选择开始时间"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"结束时间": {
|
||||||
|
"label": "结束时间",
|
||||||
|
"bind": "to",
|
||||||
|
"input": {
|
||||||
|
"type": "date",
|
||||||
|
"props": {
|
||||||
|
"placeholder": "请选择结束时间"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"primary": "城市",
|
"primary": "城市",
|
||||||
"layout": {
|
"layout": {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,13 @@
|
||||||
"comment": "服务名称",
|
"comment": "服务名称",
|
||||||
"index": true
|
"index": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "city",
|
||||||
|
"type": "string",
|
||||||
|
"length": 50,
|
||||||
|
"comment": "城市",
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "kind_id",
|
"name": "kind_id",
|
||||||
"type": "bigInteger",
|
"type": "bigInteger",
|
||||||
|
|
@ -123,30 +130,38 @@
|
||||||
"values": [
|
"values": [
|
||||||
{
|
{
|
||||||
"name": "腾讯云主机CVM",
|
"name": "腾讯云主机CVM",
|
||||||
|
"city": "北京",
|
||||||
"short_name": "云主机",
|
"short_name": "云主机",
|
||||||
"kind_id": 3,
|
"kind_id": 3,
|
||||||
"manu_id": 1,
|
"manu_id": 1,
|
||||||
|
"industries": ["教育", "能源"],
|
||||||
"price_options": ["按月订阅"]
|
"price_options": ["按月订阅"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "腾讯云磁盘",
|
"name": "腾讯云磁盘",
|
||||||
|
"city": "广州",
|
||||||
"short_name": "云磁盘",
|
"short_name": "云磁盘",
|
||||||
"kind_id": 6,
|
"kind_id": 6,
|
||||||
"manu_id": 1,
|
"manu_id": 1,
|
||||||
|
"industries": ["政府", "能源"],
|
||||||
"price_options": ["按月订阅"]
|
"price_options": ["按月订阅"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "阿里主机",
|
"name": "阿里主机",
|
||||||
|
"city": "上海",
|
||||||
"short_name": "云主机",
|
"short_name": "云主机",
|
||||||
"kind_id": 3,
|
"kind_id": 3,
|
||||||
"manu_id": 2,
|
"manu_id": 2,
|
||||||
|
"industries": ["教育", "医疗"],
|
||||||
"price_options": ["按月订阅"]
|
"price_options": ["按月订阅"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "UCloud云主机",
|
"name": "UCloud云主机",
|
||||||
"short_name": "云主机",
|
"short_name": "云主机",
|
||||||
|
"city": "上海",
|
||||||
"kind_id": 3,
|
"kind_id": 3,
|
||||||
"manu_id": 3,
|
"manu_id": 3,
|
||||||
|
"industries": ["软件", "医疗"],
|
||||||
"price_options": ["按月订阅"]
|
"price_options": ["按月订阅"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
29
xiang/apis/chart.http.json
Normal file
29
xiang/apis/chart.http.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "数据分析接口",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "数据分析接口API",
|
||||||
|
"group": "xiang/chart",
|
||||||
|
"guard": "in-process",
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"path": "/:name/data",
|
||||||
|
"method": "GET",
|
||||||
|
"process": "xiang.chart.Data",
|
||||||
|
"in": ["$param.name", ":query-param"],
|
||||||
|
"out": {
|
||||||
|
"status": 200,
|
||||||
|
"type": "application/json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/:name/setting",
|
||||||
|
"method": "GET",
|
||||||
|
"process": "xiang.chart.Setting",
|
||||||
|
"in": ["$param.name", "$query.select"],
|
||||||
|
"out": {
|
||||||
|
"status": 200,
|
||||||
|
"type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue