Compare commits

..

2 Commits
v0.0.5 ... main

Author SHA1 Message Date
fcl 1430943c48 ignore dependency error 2025-06-27 00:11:51 +08:00
Yavolte 4eef560649 add chart package 2025-06-26 14:14:18 +08:00
10 changed files with 480 additions and 13 deletions

4
go.mod
View File

@ -5,8 +5,8 @@ go 1.23.0
toolchain go1.23.10
require (
git.nobla.cn/golang/aeus v0.0.10
git.nobla.cn/golang/rest v0.1.3
git.nobla.cn/golang/aeus v0.0.11
git.nobla.cn/golang/rest v0.1.4
github.com/envoyproxy/protoc-gen-validate v1.2.1
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.6

8
go.sum
View File

@ -1,11 +1,11 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.nobla.cn/golang/aeus v0.0.10 h1:MDov2GQgV4dxFcjhiJDMwWgfEX0ROwarH3/ETP/9/ik=
git.nobla.cn/golang/aeus v0.0.10/go.mod h1:oOEwqIp6AhKKqj6sLFO8x7IycOROYHCb/2/CjF4+9CU=
git.nobla.cn/golang/aeus v0.0.11 h1:gbXIOVOQRDTIQTjw9wPVfNC9nXBaTJCABeDYmrHW2Oc=
git.nobla.cn/golang/aeus v0.0.11/go.mod h1:oOEwqIp6AhKKqj6sLFO8x7IycOROYHCb/2/CjF4+9CU=
git.nobla.cn/golang/kos v0.1.32 h1:sFVCA7vKc8dPUd0cxzwExOSPX2mmMh2IuwL6cYS1pBc=
git.nobla.cn/golang/kos v0.1.32/go.mod h1:35Z070+5oB39WcVrh5DDlnVeftL/Ccmscw2MZFe9fUg=
git.nobla.cn/golang/rest v0.1.3 h1:B+MX1teFURVxol77Ho5+SxyF61VfvXbzsh8IIajoGG0=
git.nobla.cn/golang/rest v0.1.3/go.mod h1:4viDk7VujDokpUeHQGbnSp2bkkVZEoIkWQIs/l/TTPQ=
git.nobla.cn/golang/rest v0.1.4 h1:9/XscfNXI3aPESpy8CPtVl17VSMxU9BihhedeG+h8YY=
git.nobla.cn/golang/rest v0.1.4/go.mod h1:4viDk7VujDokpUeHQGbnSp2bkkVZEoIkWQIs/l/TTPQ=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=

View File

@ -8,12 +8,19 @@ import (
)
// Menu 合并菜单
func Menu(db *gorm.DB, model *models.Menu) (err error) {
if err = db.Where("name = ?", model.Name).First(model).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
err = db.Create(model).Error
func Menu(db *gorm.DB, datas ...*models.Menu) (err error) {
tx := db.Begin()
for _, model := range datas {
if err = tx.Where("name = ?", model.Name).First(model).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
if err = tx.Create(model).Error; err != nil {
tx.Rollback()
return err
}
}
}
}
err = tx.Commit().Error
return
}
@ -31,6 +38,7 @@ func Permission(db *gorm.DB, menuName string, permission string, label string) (
return
}
// #2c7be5
// Default 合并初始化数据集
func Default(db *gorm.DB) (err error) {
var (

View File

@ -0,0 +1,78 @@
package chartjs
import (
"slices"
"sync"
)
// DataCounter 计数统计, 实现类似Pie 和 Doughnut 之类的图形
type DataCounter struct {
mutex sync.Mutex
legends map[string]*counterValue
}
func (c *DataCounter) Inc(leg string, label string, value float64) {
c.mutex.Lock()
defer c.mutex.Unlock()
v, ok := c.legends[leg]
if !ok {
v = &counterValue{
Label: leg,
Values: make(map[string]float64),
}
c.legends[leg] = v
}
v.Values[label] += value
}
func (c *DataCounter) Dec(legend string, label string, value float64) {
c.mutex.Lock()
defer c.mutex.Unlock()
v, ok := c.legends[legend]
if !ok {
return
}
if _, ok := v.Values[label]; ok {
v.Values[label] -= value
}
}
func (c *DataCounter) Data() *CounterData {
c.mutex.Lock()
defer c.mutex.Unlock()
data := &CounterData{
Lables: make([]string, 0, 5),
Datasets: make([]*CounterValue, 0, len(c.legends)),
}
for _, row := range c.legends {
for k, _ := range row.Values {
if !slices.Contains(data.Lables, k) {
data.Lables = append(data.Lables, k)
}
}
}
for _, row := range c.legends {
set := &CounterValue{
Label: row.Label,
Data: make([]float64, 0, len(data.Lables)),
}
for _, label := range data.Lables {
set.Data = append(set.Data, row.Values[label])
}
data.Datasets = append(data.Datasets, set)
}
return data
}
func NewDataCounter(legends ...string) *DataCounter {
c := &DataCounter{
legends: make(map[string]*counterValue),
}
for _, legend := range legends {
c.legends[legend] = &counterValue{
Label: legend,
Values: make(map[string]float64),
}
}
return c
}

View File

@ -0,0 +1,46 @@
package chartjs
import "time"
// TimeSeriesGroup 时间计算数据, 实现类似以时间为维度的折线图
type TimeSeriesGroup struct {
step string
values map[string]*TimeSeries
}
func (g *TimeSeriesGroup) Inc(tm time.Time, key string, value float64) {
v, ok := g.values[key]
if !ok {
v = NewTimeSeries(g.step)
g.values[key] = v
}
v.Inc(tm, value)
}
func (g *TimeSeriesGroup) Dec(tm time.Time, key string, value float64) {
v, ok := g.values[key]
if !ok {
v = NewTimeSeries(g.step)
g.values[key] = v
}
v.Dec(tm, value)
}
// Data 生成chart.js 的图表dataset
func (g *TimeSeriesGroup) Data(sts, ets int64) (values []*TimeseriesData) {
for key, row := range g.values {
data := &TimeseriesData{
Label: key,
Data: row.Values(sts, ets),
}
values = append(values, data)
}
return
}
func NewTimeSeriesGroup(step string) *TimeSeriesGroup {
return &TimeSeriesGroup{
step: step,
values: make(map[string]*TimeSeries),
}
}

View File

@ -0,0 +1,126 @@
package chartjs
import (
"sync"
"time"
)
const (
Minute = "minute"
Hour = "hour"
Day = "day"
Method = "method"
)
// 时间序列值, 实现了按时间进行分组的值计算
type TimeSeries struct {
mutex sync.Mutex
step string
values []*TimeseriesValue
}
func (s *TimeSeries) cutTimeSeries(tm time.Time) int64 {
switch s.step {
case Method:
return time.Date(tm.Year(), tm.Month(), 0, 0, 0, 0, 0, time.Local).Unix()
case Day:
return time.Date(tm.Year(), tm.Month(), tm.Day(), 0, 0, 0, 0, time.Local).Unix()
case Hour:
return time.Date(tm.Year(), tm.Month(), tm.Day(), tm.Hour(), 0, 0, 0, time.Local).Unix()
default:
return time.Date(tm.Year(), tm.Month(), tm.Day(), tm.Hour(), tm.Minute(), 0, 0, time.Local).Unix()
}
}
func (s *TimeSeries) truncateTime(ts int64) int64 {
tm := time.Unix(ts, 0)
switch s.step {
case Method:
return time.Date(tm.Year(), tm.Month(), 0, 0, 0, 0, 0, time.Local).Unix()
case Day:
return time.Date(tm.Year(), tm.Month(), tm.Day(), 0, 0, 0, 0, time.Local).Unix()
case Hour:
return time.Date(tm.Year(), tm.Month(), tm.Day(), tm.Hour(), 0, 0, 0, time.Local).Unix()
default:
return time.Date(tm.Year(), tm.Month(), tm.Day(), tm.Hour(), tm.Minute(), 0, 0, time.Local).Unix()
}
return tm.Unix()
}
func (s *TimeSeries) nextTimestamp(ts int64) int64 {
tm := time.Unix(ts, 0)
switch s.step {
case Method:
return tm.AddDate(0, 1, 0).Unix()
case Day:
return tm.AddDate(0, 0, 1).Unix()
case Hour:
return ts + 3600
default:
return ts + 60
}
}
func (s *TimeSeries) Inc(tm time.Time, value float64) {
s.mutex.Lock()
defer s.mutex.Unlock()
ts := s.cutTimeSeries(tm)
for _, v := range s.values {
if v.Timestamp == ts {
v.Value += value
return
}
}
s.values = append(s.values, &TimeseriesValue{
Timestamp: ts,
Value: value,
})
}
func (s *TimeSeries) Dec(tm time.Time, value float64) {
s.mutex.Lock()
defer s.mutex.Unlock()
ts := s.cutTimeSeries(tm)
for _, v := range s.values {
if v.Timestamp == ts {
v.Value -= value
return
}
}
}
func (s *TimeSeries) Values(sts, ets int64) []*TimeseriesValue {
var (
nextStamp int64
)
sts = s.truncateTime(sts)
ets = s.truncateTime(ets)
series := make([]*TimeseriesValue, 0, len(s.values))
nextStamp = sts
for _, row := range s.values {
for row.Timestamp > nextStamp {
series = append(series, &TimeseriesValue{
Timestamp: nextStamp,
Value: 0,
})
nextStamp = s.nextTimestamp(nextStamp)
}
series = append(series, row)
nextStamp = s.nextTimestamp(nextStamp)
}
for ets > nextStamp {
series = append(series, &TimeseriesValue{
Timestamp: nextStamp,
Value: 0,
})
nextStamp = s.nextTimestamp(nextStamp)
}
return series
}
func NewTimeSeries(step string) *TimeSeries {
return &TimeSeries{
step: step,
values: make([]*TimeseriesValue, 0, 64),
}
}

View File

@ -0,0 +1,66 @@
package chartjs
/*
{
label: 'My Time Series Data',
data: [
{ x: '2025-01-01', y: 10 },
{ x: '2025-01-02', y: 15 },
{ x: '2025-01-03', y: 8 },
{ x: '2025-01-04', y: 20 },
{ x: '2025-01-05', y: 12 }
],
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}
*/
type TimeseriesValue struct {
Timestamp int64 `json:"x"`
Value float64 `json:"y"`
}
// TimeseriesData 时间序列的数据
type TimeseriesData struct {
Label string `json:"label"`
Data []*TimeseriesValue `json:"data"`
BorderColor string `json:"borderColor,omitempty"`
BackgroundColor string `json:"backgroundColor,omitempty"`
Tension float64 `json:"tension,omitempty"`
Fill bool `json:"fill,omitempty"`
}
/**
{
labels: labels,
datasets: [
{
label: 'Dataset 1',
data: Utils.numbers(NUMBER_CFG),
backgroundColor: [
Utils.transparentize(Utils.CHART_COLORS.red, 0.5),
Utils.transparentize(Utils.CHART_COLORS.orange, 0.5),
Utils.transparentize(Utils.CHART_COLORS.yellow, 0.5),
Utils.transparentize(Utils.CHART_COLORS.green, 0.5),
Utils.transparentize(Utils.CHART_COLORS.blue, 0.5),
]
}
]
}
*/
type counterValue struct {
Label string `json:"label"`
Values map[string]float64 `json:"data"`
}
type CounterValue struct {
Label string `json:"label"`
Data []float64 `json:"data"`
BackgroundColor []string `json:"backgroundColor,omitempty"`
}
type CounterData struct {
Lables []string `json:"labels"`
Datasets []*CounterValue `json:"datasets"`
}

View File

@ -84,7 +84,7 @@ func TryCache[T any](ctx context.Context, key string, f func(tx *gorm.DB) (T, er
if opts.dependency == nil {
return entry.Value, nil
}
if dependValue, err = opts.dependency.GetValue(ctx, opts.db); err == nil {
if dependValue, err = opts.dependency.GetValue(ctx, opts.db); err == nil && dependValue != "" {
hasDependValue = true
if entry.CompareValue == dependValue {
return entry.Value, nil
@ -98,7 +98,7 @@ func TryCache[T any](ctx context.Context, key string, f func(tx *gorm.DB) (T, er
if val, err, _ = singleInstance.Do(key, func() (any, error) {
if result, err = f(tx); err == nil {
if !hasDependValue && opts.dependency != nil {
dependValue, err = opts.dependency.GetValue(ctx, tx)
dependValue, _ = opts.dependency.GetValue(ctx, tx)
}
opts.cache.Store(ctx, key, &cacheEntry[T]{
CompareValue: dependValue,

144
server.go
View File

@ -10,6 +10,7 @@ import (
"reflect"
"strconv"
"strings"
"time"
"git.nobla.cn/golang/aeus-admin/migrate"
"git.nobla.cn/golang/aeus-admin/models"
@ -334,10 +335,151 @@ func registerRESTRoute(domain string, db *gorm.DB, hs *http.Server) {
}
}
handleKeyValues := func(ctx *http.Context) (err error) {
var (
dbDependency dbcache.CacheDependency
pairs []*restTypes.TypeValue[any]
modelValue any
)
moduleName := ctx.Param("module")
tableName := ctx.Param("table")
entities := rest.GetModels()
for _, entry := range entities {
if entry.ModuleName() == moduleName && entry.TableName() == tableName {
modelValue = reflect.New(entry.Value().Type()).Interface()
for _, field := range entry.Fields() {
if field.AutoUpdateTime > 0 {
dbDependency = dbcache.NewSqlDependency(fmt.Sprintf("SELECT MAX(`%s`) FROM `%s`", field.DBName, entry.TableName()))
break
}
}
break
}
}
if modelValue == nil {
return ctx.Error(errors.NotFound, "model not found")
}
labelColumn := ctx.Param("label")
valueColumn := ctx.Param("value")
opts := make([]dbcache.CacheOption, 0, 4)
opts = append(opts, dbcache.WithDB(db))
if dbDependency != nil {
opts = append(opts, dbcache.WithCacheDuration(time.Minute*30))
opts = append(opts, dbcache.WithDependency(dbDependency))
} else {
opts = append(opts, dbcache.WithCacheDuration(time.Minute))
}
if pairs, err = dbcache.TryCache(ctx.Context(), fmt.Sprintf("rest:kvpairs:%s:%s:%s:%s", moduleName, tableName, labelColumn, valueColumn), func(tx *gorm.DB) ([]*restTypes.TypeValue[any], error) {
return rest.ModelTypes[any](ctx.Context(), db, modelValue, "", labelColumn, valueColumn)
}, opts...); err == nil {
return ctx.Success(pairs)
} else {
return ctx.Error(errors.Unavailable, err.Error())
}
}
handleTierPairs := func(ctx *http.Context) (err error) {
var (
dbDependency dbcache.CacheDependency
pairs []*restTypes.TierValue[string]
modelValue any
)
moduleName := ctx.Param("module")
tableName := ctx.Param("table")
entities := rest.GetModels()
for _, entry := range entities {
if entry.ModuleName() == moduleName && entry.TableName() == tableName {
// 权限控制
if err = entry.HasPermission(ctx.Context(), entry.Permission(restTypes.ScenarioList)); err != nil {
return ctx.Error(errors.AccessDenied, err.Error())
}
modelValue = reflect.New(entry.Value().Type()).Interface()
for _, field := range entry.Fields() {
if field.AutoUpdateTime > 0 {
dbDependency = dbcache.NewSqlDependency(fmt.Sprintf("SELECT MAX(`%s`) FROM `%s`", field.DBName, entry.TableName()))
break
}
}
break
}
}
if modelValue == nil {
return ctx.Error(errors.NotFound, "model not found")
}
parentColumn := ctx.Param("parent")
labelColumn := ctx.Param("label")
valueColumn := ctx.Param("value")
opts := make([]dbcache.CacheOption, 0, 4)
opts = append(opts, dbcache.WithDB(db))
if dbDependency != nil {
opts = append(opts, dbcache.WithCacheDuration(time.Minute*30))
opts = append(opts, dbcache.WithDependency(dbDependency))
} else {
opts = append(opts, dbcache.WithCacheDuration(time.Minute))
}
if pairs, err = dbcache.TryCache(ctx.Context(), fmt.Sprintf("rest:tierpairs:%s:%s:%s:%s", moduleName, tableName, labelColumn, valueColumn), func(tx *gorm.DB) ([]*restTypes.TierValue[string], error) {
return rest.ModelTiers[string](ctx.Context(), db, modelValue, "", parentColumn, labelColumn, valueColumn)
}, opts...); err == nil {
return ctx.Success(pairs)
} else {
return ctx.Error(errors.Unavailable, err.Error())
}
}
handleNumberTierPairs := func(ctx *http.Context) (err error) {
var (
dbDependency dbcache.CacheDependency
pairs []*restTypes.TierValue[int64]
modelValue any
)
moduleName := ctx.Param("module")
tableName := ctx.Param("table")
entities := rest.GetModels()
for _, entry := range entities {
if entry.ModuleName() == moduleName && entry.TableName() == tableName {
// 权限控制
if err = entry.HasPermission(ctx.Context(), entry.Permission(restTypes.ScenarioList)); err != nil {
return ctx.Error(errors.AccessDenied, err.Error())
}
modelValue = reflect.New(entry.Value().Type()).Interface()
for _, field := range entry.Fields() {
if field.AutoUpdateTime > 0 {
dbDependency = dbcache.NewSqlDependency(fmt.Sprintf("SELECT MAX(`%s`) FROM `%s`", field.DBName, entry.TableName()))
break
}
}
break
}
}
if modelValue == nil {
return ctx.Error(errors.NotFound, "model not found")
}
parentColumn := ctx.Param("parent")
labelColumn := ctx.Param("label")
valueColumn := ctx.Param("value")
opts := make([]dbcache.CacheOption, 0, 4)
opts = append(opts, dbcache.WithDB(db))
if dbDependency != nil {
opts = append(opts, dbcache.WithCacheDuration(time.Minute*30))
opts = append(opts, dbcache.WithDependency(dbDependency))
} else {
opts = append(opts, dbcache.WithCacheDuration(time.Minute))
}
if pairs, err = dbcache.TryCache(ctx.Context(), fmt.Sprintf("rest:tierpairs:%s:%s:%s:%s", moduleName, tableName, labelColumn, valueColumn), func(tx *gorm.DB) ([]*restTypes.TierValue[int64], error) {
return rest.ModelTiers[int64](ctx.Context(), db, modelValue, "", parentColumn, labelColumn, valueColumn)
}, opts...); err == nil {
return ctx.Success(pairs)
} else {
return ctx.Error(errors.Unavailable, err.Error())
}
}
hs.GET("/rest/schema/:module/:table", handleListSchemas)
hs.PUT("/rest/schema/:module/:table", handleUpdateSchemas)
hs.DELETE("/rest/schema/:id", handleDeleteSchema)
hs.GET("/rest/kvpairs/:module/:table/:value/:label", handleKeyValues) //处理键值对数据
hs.GET("/rest/tierpairs/str/:module/:table/:parent/:value/:label", handleTierPairs) //处理字符串类型的层级数据
hs.GET("/rest/tierpairs/num/:module/:table/:parent/:value/:label", handleNumberTierPairs) //处理数字类型的层级数据, 只支持int64
}
// AutoMigrate 自动生成一个模型的schema和权限的定义

View File

@ -17,6 +17,7 @@ type (
SettingOption func(o *settingOptions)
)
type SettingService struct {
opts *settingOptions
}