数据源和 oss

This commit is contained in:
yvan 2026-08-13 23:17:55 +08:00
parent f1a0b17bb0
commit c4bf7ee03b
40 changed files with 1975 additions and 922 deletions

3
.gitignore vendored
View File

@ -31,7 +31,7 @@ Thumbs.db
*.key
*.log
bin/
/cmd/kratos-admin
/cmd/kratos-admin/kratos-admin
# Develop tools
.vscode/
@ -41,3 +41,4 @@ bin/
web/dist/
web/node_modules/
.pnpm-store/

99
cmd/kratos-admin/main.go Normal file
View File

@ -0,0 +1,99 @@
package main
import (
"flag"
"log/slog"
"os"
"path/filepath"
"kra/internal/conf"
"kra/internal/server"
"github.com/go-kratos/kratos/contrib/otel/v3/tracing"
"github.com/go-kratos/kratos/v3"
"github.com/go-kratos/kratos/v3/config"
"github.com/go-kratos/kratos/v3/config/file"
"github.com/go-kratos/kratos/v3/log"
"github.com/go-kratos/kratos/v3/transport/grpc"
_ "go.uber.org/automaxprocs"
)
// go build -ldflags "-X main.Version=x.y.z"
var (
// Name is the name of the compiled software.
Name string
// Version is the version of the compiled software.
Version string
// flagconf is the config flag.
flagconf string
id, _ = os.Hostname()
)
func init() {
flag.StringVar(&flagconf, "conf", "../../configs", "config path, eg: -conf config.yaml")
}
func newApp(logger *slog.Logger, gs *grpc.Server, hs *server.GinServer, scheduler *server.TaskScheduler) *kratos.App {
return kratos.New(
kratos.ID(id),
kratos.Name(Name),
kratos.Version(Version),
kratos.Metadata(map[string]string{}),
kratos.Logger(logger),
kratos.Server(
gs,
hs,
scheduler,
),
)
}
func main() {
flag.Parse()
logger := log.NewLogger(
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelInfo,
}),
log.WithExtractor(tracing.TraceAttrs),
).With(
slog.String("service.id", id),
slog.String("service.name", Name),
slog.String("service.version", Version),
)
log.SetDefault(logger)
c := config.New(
config.WithSource(
file.NewSource(flagconf),
),
)
defer c.Close()
if err := c.Load(); err != nil {
panic(err)
}
var bc conf.Bootstrap
if err := c.Scan(&bc); err != nil {
panic(err)
}
if bc.Admin != nil {
bc.Admin.ConfigPath = flagconf
if info, err := os.Stat(flagconf); err == nil && info.IsDir() {
bc.Admin.ConfigPath = filepath.Join(flagconf, "config.yaml")
}
}
app, cleanup, err := wireApp(bc.Server, bc.Data, bc.Admin, logger)
if err != nil {
panic(err)
}
defer cleanup()
// start and wait for stop signal
if err := app.Run(); err != nil {
panic(err)
}
}

24
cmd/kratos-admin/wire.go Normal file
View File

@ -0,0 +1,24 @@
//go:build wireinject
// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import (
"log/slog"
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/data"
"kra/internal/server"
"kra/internal/service"
"github.com/go-kratos/kratos/v3"
"github.com/google/wire"
)
// wireApp init kratos application.
func wireApp(*conf.Server, *conf.Data, *conf.AdminBackend, *slog.Logger) (*kratos.App, func(), error) {
panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}

77
cmd/kratos-admin/wire_gen.go generated Normal file
View File

@ -0,0 +1,77 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
import (
"github.com/go-kratos/kratos/v3"
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/data"
"kra/internal/server"
"kra/internal/service"
"log/slog"
)
import (
_ "go.uber.org/automaxprocs"
)
// Injectors from wire.go:
// wireApp init kratos application.
func wireApp(confServer *conf.Server, confData *conf.Data, adminBackend *conf.AdminBackend, logger *slog.Logger) (*kratos.App, func(), error) {
dataData, cleanup, err := data.NewData(confData, adminBackend)
if err != nil {
return nil, nil, err
}
adminRepo := data.NewAdminRepo(dataData)
adminUsecase := biz.NewAdminUsecase(adminRepo)
adminService := service.NewAdminService(adminUsecase)
grpcServer := server.NewGRPCServer(confServer, adminService)
systemRepo := data.NewSystemRepo(dataData)
cache := data.NewCache(dataData)
fileStorage, err := data.NewFileStorage(adminBackend)
if err != nil {
cleanup()
return nil, nil, err
}
systemUsecase := biz.NewSystemUsecase(systemRepo, cache, fileStorage)
settingsRepo := data.NewSettingsRepo(dataData)
settingsUsecase := biz.NewSettingsUsecase(settingsRepo)
settingsService := service.NewSettingsService(settingsUsecase, adminBackend)
systemService := service.NewSystemService(systemUsecase, adminBackend, settingsService)
accessRepo := data.NewAccessRepo(dataData)
accessUsecase := biz.NewAccessUsecase(accessRepo)
accessService := service.NewAccessService(accessUsecase)
versionRepo := data.NewVersionRepo(dataData)
versionUsecase := biz.NewVersionUsecase(versionRepo)
versionService := service.NewVersionService(versionUsecase)
exportRepo := data.NewExportRepo(dataData)
exportUsecase := biz.NewExportUsecase(exportRepo)
exportService := service.NewExportService(exportUsecase)
auditRepo := data.NewAuditRepo(dataData)
auditUsecase := biz.NewAuditUsecase(auditRepo)
auditService := service.NewAuditService(auditUsecase)
taskRepo := data.NewTaskRepo(dataData)
taskUsecase := biz.NewTaskUsecase(taskRepo)
taskService := service.NewTaskService(taskUsecase)
mediaRepo := data.NewMediaRepo(dataData)
mediaUsecase := biz.NewMediaUsecase(mediaRepo, fileStorage)
mediaService := service.NewMediaService(mediaUsecase)
announcementRepo := data.NewAnnouncementRepo(dataData)
announcementUsecase := biz.NewAnnouncementUsecase(announcementRepo)
announcementService := service.NewAnnouncementService(announcementUsecase)
emailRepo := data.NewEmailRepo(adminBackend)
emailUsecase := biz.NewEmailUsecase(emailRepo)
emailService := service.NewEmailService(emailUsecase)
taskScheduler := server.NewTaskScheduler(taskService, logger)
ginServer := server.NewGinServer(confServer, adminBackend, systemService, accessService, settingsService, versionService, exportService, auditService, taskService, mediaService, announcementService, emailService, taskScheduler, logger)
app := newApp(logger, grpcServer, ginServer, taskScheduler)
return app, func() {
cleanup()
}, nil
}

View File

@ -9,6 +9,12 @@ data:
database:
driver: mysql
source: root:root@tcp(127.0.0.1:3306)/test?timeout=5s&parseTime=True&loc=Local&charset=utf8mb4
host: 127.0.0.1
port: "3306"
user: root
password: root
name: test
config: timeout=5s&parseTime=True&loc=Local&charset=utf8mb4
redis:
addr: 127.0.0.1:6379
read_timeout: 0.2s
@ -18,17 +24,21 @@ admin:
jwt:
# Production deployments must override this value with a private secret.
signing_key: change-me-before-production
expires_time: 168h
buffer_time: 24h
expires_time: 604800s
buffer_time: 86400s
issuer: kra
captcha:
key_long: 6
img_width: 240
img_height: 80
store_expiration: 3m
store_expiration: 180s
local:
store_path: uploads/file
path_prefix: uploads/file
storage:
# local, qiniu, aliyun-oss, huawei-obs, tencent-cos, aws-s3,
# cloudflare-r2 or minio
type: local
email:
# Leave host/from/secret empty to disable SMTP error notifications.
to: ""

52
go.mod
View File

@ -5,17 +5,20 @@ go 1.25.7
require (
entgo.io/ent v0.14.6
github.com/casbin/casbin/v3 v3.10.0
github.com/dzwvip/gorm-oracle v0.1.2
github.com/gin-gonic/gin v1.10.0
github.com/go-kratos/aip-go/ents v0.0.0-20251213081434-74ffa1fc1588
github.com/glebarez/sqlite v1.11.0
github.com/go-kratos/kratos/contrib/otel/v3 v3.0.0-20260617100506-4e232a3eff59
github.com/go-kratos/kratos/v3 v3.0.0
github.com/go-sql-driver/mysql v1.10.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/google/wire v0.7.0
github.com/minio/minio-go/v7 v7.0.92
github.com/mojocn/base64Captcha v1.3.8
github.com/qiniu/go-sdk/v7 v7.25.2
github.com/redis/go-redis/v9 v9.7.0
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.7
github.com/xuri/excelize/v2 v2.9.0
go.einride.tech/aip v0.86.3
go.uber.org/automaxprocs v1.6.0
@ -23,7 +26,10 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlserver v1.6.3
gorm.io/gorm v1.31.1
)
@ -32,7 +38,9 @@ replace github.com/go-kratos/kratos/v3 v3.0.0 => github.com/go-kratos/kratos/v3
require (
ariga.io/atlas v1.2.2 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
@ -43,39 +51,69 @@ require (
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gammazero/toposort v0.1.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-openapi/inflect v0.21.6 // indirect
github.com/go-playground/form/v4 v4.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/go-sql-driver/mysql v1.10.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/hcl/v2 v2.24.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/microsoft/go-mssqldb v1.8.2 // indirect
github.com/minio/crc64nvme v1.0.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/sijms/go-ora/v2 v2.7.17 // indirect
github.com/thoas/go-funk v0.7.0 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zclconf/go-cty v1.18.1 // indirect
github.com/zclconf/go-cty-yaml v1.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
@ -91,5 +129,9 @@ require (
golang.org/x/text v0.38.0 // indirect
golang.org/x/tools v0.46.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/fileutil v1.0.0 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
)

226
go.sum
View File

@ -6,10 +6,32 @@ entgo.io/ent v0.14.6 h1:/f2696BpwuWAEEG6PVGWflg6+Inrpq4pRWuNlWz/Skk=
entgo.io/ent v0.14.6/go.mod h1:z46QBUdGC+BATwsedbDuREfSS0oSCV+csdEYlL4p73s=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4=
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
@ -36,11 +58,23 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dzwvip/gorm-oracle v0.1.2 h1:811aFDY7oDfKWHc0Z0lHdXzzr89EmKBSwc/jLJ8GU5g=
github.com/dzwvip/gorm-oracle v0.1.2/go.mod h1:TbF7idnO9UgGpJ0qJpDZby1/wGquzP5GYof88ScBITE=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
@ -50,12 +84,18 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg=
github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-kratos/aip-go/ents v0.0.0-20251213081434-74ffa1fc1588 h1:e0dWyNWFeTgGCH7cRMROahTwMaQYtmHce/6fxVmA6yI=
github.com/go-kratos/aip-go/ents v0.0.0-20251213081434-74ffa1fc1588/go.mod h1:ifKMm4eJmaQz5WNKKhfClpCng8UxUB3sArof8wjwG78=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kratos/kratos/contrib/otel/v3 v3.0.0-20260617100506-4e232a3eff59 h1:FeDujUZF6a2pPS0RWGI52IXWPqVcGW1QP5vHDlAqLE8=
github.com/go-kratos/kratos/contrib/otel/v3 v3.0.0-20260617100506-4e232a3eff59/go.mod h1:hT2QNZ/0DPOvRqVqxoBBL/aMCOHhbbWh92mF87DBiCU=
github.com/go-kratos/kratos/v3 v3.0.0-20260617100506-4e232a3eff59 h1:32laNndL0GsYLuGwWquB6if8yrBQWEzXlkkXImcfeKA=
@ -65,66 +105,128 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/inflect v0.21.6 h1:0Se5BlyDT4hnV9JQQKA63W9TIUU6SC4hPJiY3qJOEzQ=
github.com/go-openapi/inflect v0.21.6/go.mod h1:ksYcnLD7j24H79hdqOMmWaLXjFXd0LTkRoBA0UazLW8=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk=
github.com/go-playground/form/v4 v4.3.0/go.mod h1:Cpe1iYJKoXb1vILRXEwxpWMGWyQuqplQ/4cvPecy+Jo=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY=
github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.92 h1:jpBFWyRS3p8P/9tsRc+NuvqoFi7qAmTCFPoRFmobbVw=
github.com/minio/minio-go/v7 v7.0.92/go.mod h1:vTIc8DNcnAZIhyFsk8EB90AbPjj3j68aWIEQCiPj7d0=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -132,20 +234,37 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg=
github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
github.com/qiniu/go-sdk/v7 v7.25.2 h1:URwgZpxySdiwu2yQpHk93X4LXWHyFRp1x3Vmlk/YWvo=
github.com/qiniu/go-sdk/v7 v7.25.2/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peqTowyTO8o=
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
@ -153,13 +272,25 @@ github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
github.com/sijms/go-ora/v2 v2.7.17 h1:M/pYIqjaMUeBxyzOWp2oj4ntF6fHSBloJWGNH9vbmsU=
github.com/sijms/go-ora/v2 v2.7.17/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@ -168,6 +299,14 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y=
github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
@ -179,6 +318,8 @@ github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmji
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM=
github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
@ -205,29 +346,48 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -236,38 +396,64 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
@ -280,6 +466,7 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d h1:/hmn0Ku5kWij/kjGsrcJeC1T/MrJi2iNWwgAqrihFwc=
@ -293,16 +480,39 @@ google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zN
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI=
gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w=
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -35,6 +35,10 @@ type UserListFilter struct {
Desc bool
}
type DatabaseConfig struct {
Driver, Host, Port, User, Password, Name, Path, Config, AdminPassword string
}
type Authority struct {
AuthorityID uint
AuthorityName string
@ -64,7 +68,7 @@ type Menu struct {
type SystemRepo interface {
IsInitialized(context.Context) (bool, error)
Initialize(context.Context) error
Initialize(context.Context, *DatabaseConfig) error
FindUserByUsername(context.Context, string) (*User, error)
FindUserByID(context.Context, uint) (*User, error)
MenusByAuthority(context.Context, uint) ([]*Menu, error)
@ -72,11 +76,12 @@ type SystemRepo interface {
CreateUser(context.Context, *User) (*User, error)
UpdateUser(context.Context, *User) error
DeleteUser(context.Context, uint) error
UpdatePassword(context.Context, uint, string) error
UpdatePassword(context.Context, uint, string, bool) error
SetUserAuthorities(context.Context, uint, []uint) error
SetUserAuthority(context.Context, uint, uint) error
SetUserSetting(context.Context, uint, map[string]any) error
ListAuthorities(context.Context) ([]*Authority, error)
PersistConfig(context.Context) error
}
type SystemUsecase struct {
@ -102,7 +107,13 @@ func (uc *SystemUsecase) CacheDelete(ctx context.Context, key string) error {
func (uc *SystemUsecase) IsInitialized(ctx context.Context) (bool, error) {
return uc.repo.IsInitialized(ctx)
}
func (uc *SystemUsecase) Initialize(ctx context.Context) error { return uc.repo.Initialize(ctx) }
func (uc *SystemUsecase) Initialize(ctx context.Context, config *DatabaseConfig) error {
if config == nil || len(config.AdminPassword) < 6 {
return ErrInvalidCredentials
}
return uc.repo.Initialize(ctx, config)
}
func (uc *SystemUsecase) PersistConfig(ctx context.Context) error { return uc.repo.PersistConfig(ctx) }
func (uc *SystemUsecase) Login(ctx context.Context, username, password string) (*User, error) {
u, err := uc.repo.FindUserByUsername(ctx, username)
@ -142,7 +153,7 @@ func (uc *SystemUsecase) ResetPassword(ctx context.Context, id uint, password st
if err != nil {
return err
}
return uc.repo.UpdatePassword(ctx, id, string(hash))
return uc.repo.UpdatePassword(ctx, id, string(hash), false)
}
func (uc *SystemUsecase) ChangePassword(ctx context.Context, id uint, oldPassword, newPassword string) error {
user, err := uc.repo.FindUserByID(ctx, id)
@ -152,7 +163,11 @@ func (uc *SystemUsecase) ChangePassword(ctx context.Context, id uint, oldPasswor
if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword)) != nil {
return ErrInvalidCredentials
}
return uc.ResetPassword(ctx, id, newPassword)
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
return uc.repo.UpdatePassword(ctx, id, string(hash), true)
}
func (uc *SystemUsecase) Authorities(ctx context.Context) ([]*Authority, error) {
return uc.repo.ListAuthorities(ctx)

522
internal/conf/conf.pb.go generated
View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.35.1
// protoc (unknown)
// source: conf/conf.proto
package conf
@ -188,12 +188,15 @@ func (x *Data) GetRedis() *Data_Redis {
// AdminBackend contains settings for the administration HTTP transport.
type AdminBackend struct {
state protoimpl.MessageState `protogen:"open.v1"`
RouterPrefix string `protobuf:"bytes,1,opt,name=router_prefix,json=routerPrefix,proto3" json:"router_prefix,omitempty"`
Jwt *AdminBackend_JWT `protobuf:"bytes,2,opt,name=jwt,proto3" json:"jwt,omitempty"`
Captcha *AdminBackend_Captcha `protobuf:"bytes,3,opt,name=captcha,proto3" json:"captcha,omitempty"`
Local *AdminBackend_Local `protobuf:"bytes,4,opt,name=local,proto3" json:"local,omitempty"`
Email *AdminBackend_Email `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
RouterPrefix string `protobuf:"bytes,1,opt,name=router_prefix,json=routerPrefix,proto3" json:"router_prefix,omitempty"`
Jwt *AdminBackend_JWT `protobuf:"bytes,2,opt,name=jwt,proto3" json:"jwt,omitempty"`
Captcha *AdminBackend_Captcha `protobuf:"bytes,3,opt,name=captcha,proto3" json:"captcha,omitempty"`
Local *AdminBackend_Local `protobuf:"bytes,4,opt,name=local,proto3" json:"local,omitempty"`
Email *AdminBackend_Email `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"`
Storage *AdminBackend_Storage `protobuf:"bytes,6,opt,name=storage,proto3" json:"storage,omitempty"`
// ConfigPath is populated by the entrypoint and is not required in YAML.
ConfigPath string `protobuf:"bytes,7,opt,name=config_path,json=configPath,proto3" json:"config_path,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -263,6 +266,20 @@ func (x *AdminBackend) GetEmail() *AdminBackend_Email {
return nil
}
func (x *AdminBackend) GetStorage() *AdminBackend_Storage {
if x != nil {
return x.Storage
}
return nil
}
func (x *AdminBackend) GetConfigPath() string {
if x != nil {
return x.ConfigPath
}
return ""
}
type Server_HTTP struct {
state protoimpl.MessageState `protogen:"open.v1"`
Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"`
@ -387,6 +404,13 @@ type Data_Database struct {
state protoimpl.MessageState `protogen:"open.v1"`
Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"`
Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"`
Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
Port string `protobuf:"bytes,4,opt,name=port,proto3" json:"port,omitempty"`
User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"`
Password string `protobuf:"bytes,6,opt,name=password,proto3" json:"password,omitempty"`
Name string `protobuf:"bytes,7,opt,name=name,proto3" json:"name,omitempty"`
Config string `protobuf:"bytes,8,opt,name=config,proto3" json:"config,omitempty"`
Path string `protobuf:"bytes,9,opt,name=path,proto3" json:"path,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -435,6 +459,55 @@ func (x *Data_Database) GetSource() string {
return ""
}
func (x *Data_Database) GetHost() string {
if x != nil {
return x.Host
}
return ""
}
func (x *Data_Database) GetPort() string {
if x != nil {
return x.Port
}
return ""
}
func (x *Data_Database) GetUser() string {
if x != nil {
return x.User
}
return ""
}
func (x *Data_Database) GetPassword() string {
if x != nil {
return x.Password
}
return ""
}
func (x *Data_Database) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Data_Database) GetConfig() string {
if x != nil {
return x.Config
}
return ""
}
func (x *Data_Database) GetPath() string {
if x != nil {
return x.Path
}
return ""
}
type Data_Redis struct {
state protoimpl.MessageState `protogen:"open.v1"`
Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"`
@ -791,6 +864,316 @@ func (x *AdminBackend_Email) GetIsLoginAuth() bool {
return false
}
type AdminBackend_Storage struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Qiniu *AdminBackend_Qiniu `protobuf:"bytes,2,opt,name=qiniu,proto3" json:"qiniu,omitempty"`
AliyunOss *AdminBackend_ObjectStore `protobuf:"bytes,3,opt,name=aliyun_oss,json=aliyunOss,proto3" json:"aliyun_oss,omitempty"`
HuaweiObs *AdminBackend_ObjectStore `protobuf:"bytes,4,opt,name=huawei_obs,json=huaweiObs,proto3" json:"huawei_obs,omitempty"`
TencentCos *AdminBackend_ObjectStore `protobuf:"bytes,5,opt,name=tencent_cos,json=tencentCos,proto3" json:"tencent_cos,omitempty"`
AwsS3 *AdminBackend_ObjectStore `protobuf:"bytes,6,opt,name=aws_s3,json=awsS3,proto3" json:"aws_s3,omitempty"`
CloudflareR2 *AdminBackend_ObjectStore `protobuf:"bytes,7,opt,name=cloudflare_r2,json=cloudflareR2,proto3" json:"cloudflare_r2,omitempty"`
Minio *AdminBackend_ObjectStore `protobuf:"bytes,8,opt,name=minio,proto3" json:"minio,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AdminBackend_Storage) Reset() {
*x = AdminBackend_Storage{}
mi := &file_conf_conf_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AdminBackend_Storage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AdminBackend_Storage) ProtoMessage() {}
func (x *AdminBackend_Storage) ProtoReflect() protoreflect.Message {
mi := &file_conf_conf_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AdminBackend_Storage.ProtoReflect.Descriptor instead.
func (*AdminBackend_Storage) Descriptor() ([]byte, []int) {
return file_conf_conf_proto_rawDescGZIP(), []int{3, 4}
}
func (x *AdminBackend_Storage) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *AdminBackend_Storage) GetQiniu() *AdminBackend_Qiniu {
if x != nil {
return x.Qiniu
}
return nil
}
func (x *AdminBackend_Storage) GetAliyunOss() *AdminBackend_ObjectStore {
if x != nil {
return x.AliyunOss
}
return nil
}
func (x *AdminBackend_Storage) GetHuaweiObs() *AdminBackend_ObjectStore {
if x != nil {
return x.HuaweiObs
}
return nil
}
func (x *AdminBackend_Storage) GetTencentCos() *AdminBackend_ObjectStore {
if x != nil {
return x.TencentCos
}
return nil
}
func (x *AdminBackend_Storage) GetAwsS3() *AdminBackend_ObjectStore {
if x != nil {
return x.AwsS3
}
return nil
}
func (x *AdminBackend_Storage) GetCloudflareR2() *AdminBackend_ObjectStore {
if x != nil {
return x.CloudflareR2
}
return nil
}
func (x *AdminBackend_Storage) GetMinio() *AdminBackend_ObjectStore {
if x != nil {
return x.Minio
}
return nil
}
type AdminBackend_Qiniu struct {
state protoimpl.MessageState `protogen:"open.v1"`
Zone string `protobuf:"bytes,1,opt,name=zone,proto3" json:"zone,omitempty"`
Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"`
BaseUrl string `protobuf:"bytes,3,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"`
AccessKey string `protobuf:"bytes,4,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"`
SecretKey string `protobuf:"bytes,5,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"`
UseHttps bool `protobuf:"varint,6,opt,name=use_https,json=useHttps,proto3" json:"use_https,omitempty"`
UseCdnDomains bool `protobuf:"varint,7,opt,name=use_cdn_domains,json=useCdnDomains,proto3" json:"use_cdn_domains,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AdminBackend_Qiniu) Reset() {
*x = AdminBackend_Qiniu{}
mi := &file_conf_conf_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AdminBackend_Qiniu) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AdminBackend_Qiniu) ProtoMessage() {}
func (x *AdminBackend_Qiniu) ProtoReflect() protoreflect.Message {
mi := &file_conf_conf_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AdminBackend_Qiniu.ProtoReflect.Descriptor instead.
func (*AdminBackend_Qiniu) Descriptor() ([]byte, []int) {
return file_conf_conf_proto_rawDescGZIP(), []int{3, 5}
}
func (x *AdminBackend_Qiniu) GetZone() string {
if x != nil {
return x.Zone
}
return ""
}
func (x *AdminBackend_Qiniu) GetBucket() string {
if x != nil {
return x.Bucket
}
return ""
}
func (x *AdminBackend_Qiniu) GetBaseUrl() string {
if x != nil {
return x.BaseUrl
}
return ""
}
func (x *AdminBackend_Qiniu) GetAccessKey() string {
if x != nil {
return x.AccessKey
}
return ""
}
func (x *AdminBackend_Qiniu) GetSecretKey() string {
if x != nil {
return x.SecretKey
}
return ""
}
func (x *AdminBackend_Qiniu) GetUseHttps() bool {
if x != nil {
return x.UseHttps
}
return false
}
func (x *AdminBackend_Qiniu) GetUseCdnDomains() bool {
if x != nil {
return x.UseCdnDomains
}
return false
}
// ObjectStore covers S3-compatible configuration used by public cloud
// providers and self-hosted MinIO.
type AdminBackend_ObjectStore struct {
state protoimpl.MessageState `protogen:"open.v1"`
Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
Region string `protobuf:"bytes,2,opt,name=region,proto3" json:"region,omitempty"`
Bucket string `protobuf:"bytes,3,opt,name=bucket,proto3" json:"bucket,omitempty"`
AccessKey string `protobuf:"bytes,4,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"`
SecretKey string `protobuf:"bytes,5,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"`
BaseUrl string `protobuf:"bytes,6,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"`
PathPrefix string `protobuf:"bytes,7,opt,name=path_prefix,json=pathPrefix,proto3" json:"path_prefix,omitempty"`
UseSsl bool `protobuf:"varint,8,opt,name=use_ssl,json=useSsl,proto3" json:"use_ssl,omitempty"`
ForcePathStyle bool `protobuf:"varint,9,opt,name=force_path_style,json=forcePathStyle,proto3" json:"force_path_style,omitempty"`
AccountId string `protobuf:"bytes,10,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AdminBackend_ObjectStore) Reset() {
*x = AdminBackend_ObjectStore{}
mi := &file_conf_conf_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AdminBackend_ObjectStore) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AdminBackend_ObjectStore) ProtoMessage() {}
func (x *AdminBackend_ObjectStore) ProtoReflect() protoreflect.Message {
mi := &file_conf_conf_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AdminBackend_ObjectStore.ProtoReflect.Descriptor instead.
func (*AdminBackend_ObjectStore) Descriptor() ([]byte, []int) {
return file_conf_conf_proto_rawDescGZIP(), []int{3, 6}
}
func (x *AdminBackend_ObjectStore) GetEndpoint() string {
if x != nil {
return x.Endpoint
}
return ""
}
func (x *AdminBackend_ObjectStore) GetRegion() string {
if x != nil {
return x.Region
}
return ""
}
func (x *AdminBackend_ObjectStore) GetBucket() string {
if x != nil {
return x.Bucket
}
return ""
}
func (x *AdminBackend_ObjectStore) GetAccessKey() string {
if x != nil {
return x.AccessKey
}
return ""
}
func (x *AdminBackend_ObjectStore) GetSecretKey() string {
if x != nil {
return x.SecretKey
}
return ""
}
func (x *AdminBackend_ObjectStore) GetBaseUrl() string {
if x != nil {
return x.BaseUrl
}
return ""
}
func (x *AdminBackend_ObjectStore) GetPathPrefix() string {
if x != nil {
return x.PathPrefix
}
return ""
}
func (x *AdminBackend_ObjectStore) GetUseSsl() bool {
if x != nil {
return x.UseSsl
}
return false
}
func (x *AdminBackend_ObjectStore) GetForcePathStyle() bool {
if x != nil {
return x.ForcePathStyle
}
return false
}
func (x *AdminBackend_ObjectStore) GetAccountId() string {
if x != nil {
return x.AccountId
}
return ""
}
var File_conf_conf_proto protoreflect.FileDescriptor
const file_conf_conf_proto_rawDesc = "" +
@ -811,24 +1194,34 @@ const file_conf_conf_proto_rawDesc = "" +
"\x04GRPC\x12\x18\n" +
"\anetwork\x18\x01 \x01(\tR\anetwork\x12\x12\n" +
"\x04addr\x18\x02 \x01(\tR\x04addr\x123\n" +
"\atimeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\xdd\x02\n" +
"\atimeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\xf6\x03\n" +
"\x04Data\x125\n" +
"\bdatabase\x18\x01 \x01(\v2\x19.kratos.api.Data.DatabaseR\bdatabase\x12,\n" +
"\x05redis\x18\x02 \x01(\v2\x16.kratos.api.Data.RedisR\x05redis\x1a:\n" +
"\x05redis\x18\x02 \x01(\v2\x16.kratos.api.Data.RedisR\x05redis\x1a\xd2\x01\n" +
"\bDatabase\x12\x16\n" +
"\x06driver\x18\x01 \x01(\tR\x06driver\x12\x16\n" +
"\x06source\x18\x02 \x01(\tR\x06source\x1a\xb3\x01\n" +
"\x06source\x18\x02 \x01(\tR\x06source\x12\x12\n" +
"\x04host\x18\x03 \x01(\tR\x04host\x12\x12\n" +
"\x04port\x18\x04 \x01(\tR\x04port\x12\x12\n" +
"\x04user\x18\x05 \x01(\tR\x04user\x12\x1a\n" +
"\bpassword\x18\x06 \x01(\tR\bpassword\x12\x12\n" +
"\x04name\x18\a \x01(\tR\x04name\x12\x16\n" +
"\x06config\x18\b \x01(\tR\x06config\x12\x12\n" +
"\x04path\x18\t \x01(\tR\x04path\x1a\xb3\x01\n" +
"\x05Redis\x12\x18\n" +
"\anetwork\x18\x01 \x01(\tR\anetwork\x12\x12\n" +
"\x04addr\x18\x02 \x01(\tR\x04addr\x12<\n" +
"\fread_timeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\vreadTimeout\x12>\n" +
"\rwrite_timeout\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\fwriteTimeout\"\xfd\x06\n" +
"\rwrite_timeout\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\fwriteTimeout\"\xd1\x0f\n" +
"\fAdminBackend\x12#\n" +
"\rrouter_prefix\x18\x01 \x01(\tR\frouterPrefix\x12.\n" +
"\x03jwt\x18\x02 \x01(\v2\x1c.kratos.api.AdminBackend.JWTR\x03jwt\x12:\n" +
"\acaptcha\x18\x03 \x01(\v2 .kratos.api.AdminBackend.CaptchaR\acaptcha\x124\n" +
"\x05local\x18\x04 \x01(\v2\x1e.kratos.api.AdminBackend.LocalR\x05local\x124\n" +
"\x05email\x18\x05 \x01(\v2\x1e.kratos.api.AdminBackend.EmailR\x05email\x1a\xb8\x01\n" +
"\x05email\x18\x05 \x01(\v2\x1e.kratos.api.AdminBackend.EmailR\x05email\x12:\n" +
"\astorage\x18\x06 \x01(\v2 .kratos.api.AdminBackend.StorageR\astorage\x12\x1f\n" +
"\vconfig_path\x18\a \x01(\tR\n" +
"configPath\x1a\xb8\x01\n" +
"\x03JWT\x12\x1f\n" +
"\vsigning_key\x18\x01 \x01(\tR\n" +
"signingKey\x12<\n" +
@ -855,7 +1248,45 @@ const file_conf_conf_proto_rawDesc = "" +
"\bnickname\x18\x05 \x01(\tR\bnickname\x12\x12\n" +
"\x04port\x18\x06 \x01(\x05R\x04port\x12\x15\n" +
"\x06is_ssl\x18\a \x01(\bR\x05isSsl\x12\"\n" +
"\ris_login_auth\x18\b \x01(\bR\visLoginAuthB\x18Z\x16kra/internal/conf;confb\x06proto3"
"\ris_login_auth\x18\b \x01(\bR\visLoginAuth\x1a\xe8\x03\n" +
"\aStorage\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x124\n" +
"\x05qiniu\x18\x02 \x01(\v2\x1e.kratos.api.AdminBackend.QiniuR\x05qiniu\x12C\n" +
"\n" +
"aliyun_oss\x18\x03 \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\taliyunOss\x12C\n" +
"\n" +
"huawei_obs\x18\x04 \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\thuaweiObs\x12E\n" +
"\vtencent_cos\x18\x05 \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\n" +
"tencentCos\x12;\n" +
"\x06aws_s3\x18\x06 \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\x05awsS3\x12I\n" +
"\rcloudflare_r2\x18\a \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\fcloudflareR2\x12:\n" +
"\x05minio\x18\b \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\x05minio\x1a\xd1\x01\n" +
"\x05Qiniu\x12\x12\n" +
"\x04zone\x18\x01 \x01(\tR\x04zone\x12\x16\n" +
"\x06bucket\x18\x02 \x01(\tR\x06bucket\x12\x19\n" +
"\bbase_url\x18\x03 \x01(\tR\abaseUrl\x12\x1d\n" +
"\n" +
"access_key\x18\x04 \x01(\tR\taccessKey\x12\x1d\n" +
"\n" +
"secret_key\x18\x05 \x01(\tR\tsecretKey\x12\x1b\n" +
"\tuse_https\x18\x06 \x01(\bR\buseHttps\x12&\n" +
"\x0fuse_cdn_domains\x18\a \x01(\bR\ruseCdnDomains\x1a\xb5\x02\n" +
"\vObjectStore\x12\x1a\n" +
"\bendpoint\x18\x01 \x01(\tR\bendpoint\x12\x16\n" +
"\x06region\x18\x02 \x01(\tR\x06region\x12\x16\n" +
"\x06bucket\x18\x03 \x01(\tR\x06bucket\x12\x1d\n" +
"\n" +
"access_key\x18\x04 \x01(\tR\taccessKey\x12\x1d\n" +
"\n" +
"secret_key\x18\x05 \x01(\tR\tsecretKey\x12\x19\n" +
"\bbase_url\x18\x06 \x01(\tR\abaseUrl\x12\x1f\n" +
"\vpath_prefix\x18\a \x01(\tR\n" +
"pathPrefix\x12\x17\n" +
"\ause_ssl\x18\b \x01(\bR\x06useSsl\x12(\n" +
"\x10force_path_style\x18\t \x01(\bR\x0eforcePathStyle\x12\x1d\n" +
"\n" +
"account_id\x18\n" +
" \x01(\tR\taccountIdB\x18Z\x16kra/internal/conf;confb\x06proto3"
var (
file_conf_conf_proto_rawDescOnce sync.Once
@ -869,21 +1300,24 @@ func file_conf_conf_proto_rawDescGZIP() []byte {
return file_conf_conf_proto_rawDescData
}
var file_conf_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_conf_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
var file_conf_conf_proto_goTypes = []any{
(*Bootstrap)(nil), // 0: kratos.api.Bootstrap
(*Server)(nil), // 1: kratos.api.Server
(*Data)(nil), // 2: kratos.api.Data
(*AdminBackend)(nil), // 3: kratos.api.AdminBackend
(*Server_HTTP)(nil), // 4: kratos.api.Server.HTTP
(*Server_GRPC)(nil), // 5: kratos.api.Server.GRPC
(*Data_Database)(nil), // 6: kratos.api.Data.Database
(*Data_Redis)(nil), // 7: kratos.api.Data.Redis
(*AdminBackend_JWT)(nil), // 8: kratos.api.AdminBackend.JWT
(*AdminBackend_Captcha)(nil), // 9: kratos.api.AdminBackend.Captcha
(*AdminBackend_Local)(nil), // 10: kratos.api.AdminBackend.Local
(*AdminBackend_Email)(nil), // 11: kratos.api.AdminBackend.Email
(*durationpb.Duration)(nil), // 12: google.protobuf.Duration
(*Bootstrap)(nil), // 0: kratos.api.Bootstrap
(*Server)(nil), // 1: kratos.api.Server
(*Data)(nil), // 2: kratos.api.Data
(*AdminBackend)(nil), // 3: kratos.api.AdminBackend
(*Server_HTTP)(nil), // 4: kratos.api.Server.HTTP
(*Server_GRPC)(nil), // 5: kratos.api.Server.GRPC
(*Data_Database)(nil), // 6: kratos.api.Data.Database
(*Data_Redis)(nil), // 7: kratos.api.Data.Redis
(*AdminBackend_JWT)(nil), // 8: kratos.api.AdminBackend.JWT
(*AdminBackend_Captcha)(nil), // 9: kratos.api.AdminBackend.Captcha
(*AdminBackend_Local)(nil), // 10: kratos.api.AdminBackend.Local
(*AdminBackend_Email)(nil), // 11: kratos.api.AdminBackend.Email
(*AdminBackend_Storage)(nil), // 12: kratos.api.AdminBackend.Storage
(*AdminBackend_Qiniu)(nil), // 13: kratos.api.AdminBackend.Qiniu
(*AdminBackend_ObjectStore)(nil), // 14: kratos.api.AdminBackend.ObjectStore
(*durationpb.Duration)(nil), // 15: google.protobuf.Duration
}
var file_conf_conf_proto_depIdxs = []int32{
1, // 0: kratos.api.Bootstrap.server:type_name -> kratos.api.Server
@ -897,18 +1331,26 @@ var file_conf_conf_proto_depIdxs = []int32{
9, // 8: kratos.api.AdminBackend.captcha:type_name -> kratos.api.AdminBackend.Captcha
10, // 9: kratos.api.AdminBackend.local:type_name -> kratos.api.AdminBackend.Local
11, // 10: kratos.api.AdminBackend.email:type_name -> kratos.api.AdminBackend.Email
12, // 11: kratos.api.Server.HTTP.timeout:type_name -> google.protobuf.Duration
12, // 12: kratos.api.Server.GRPC.timeout:type_name -> google.protobuf.Duration
12, // 13: kratos.api.Data.Redis.read_timeout:type_name -> google.protobuf.Duration
12, // 14: kratos.api.Data.Redis.write_timeout:type_name -> google.protobuf.Duration
12, // 15: kratos.api.AdminBackend.JWT.expires_time:type_name -> google.protobuf.Duration
12, // 16: kratos.api.AdminBackend.JWT.buffer_time:type_name -> google.protobuf.Duration
12, // 17: kratos.api.AdminBackend.Captcha.store_expiration:type_name -> google.protobuf.Duration
18, // [18:18] is the sub-list for method output_type
18, // [18:18] is the sub-list for method input_type
18, // [18:18] is the sub-list for extension type_name
18, // [18:18] is the sub-list for extension extendee
0, // [0:18] is the sub-list for field type_name
12, // 11: kratos.api.AdminBackend.storage:type_name -> kratos.api.AdminBackend.Storage
15, // 12: kratos.api.Server.HTTP.timeout:type_name -> google.protobuf.Duration
15, // 13: kratos.api.Server.GRPC.timeout:type_name -> google.protobuf.Duration
15, // 14: kratos.api.Data.Redis.read_timeout:type_name -> google.protobuf.Duration
15, // 15: kratos.api.Data.Redis.write_timeout:type_name -> google.protobuf.Duration
15, // 16: kratos.api.AdminBackend.JWT.expires_time:type_name -> google.protobuf.Duration
15, // 17: kratos.api.AdminBackend.JWT.buffer_time:type_name -> google.protobuf.Duration
15, // 18: kratos.api.AdminBackend.Captcha.store_expiration:type_name -> google.protobuf.Duration
13, // 19: kratos.api.AdminBackend.Storage.qiniu:type_name -> kratos.api.AdminBackend.Qiniu
14, // 20: kratos.api.AdminBackend.Storage.aliyun_oss:type_name -> kratos.api.AdminBackend.ObjectStore
14, // 21: kratos.api.AdminBackend.Storage.huawei_obs:type_name -> kratos.api.AdminBackend.ObjectStore
14, // 22: kratos.api.AdminBackend.Storage.tencent_cos:type_name -> kratos.api.AdminBackend.ObjectStore
14, // 23: kratos.api.AdminBackend.Storage.aws_s3:type_name -> kratos.api.AdminBackend.ObjectStore
14, // 24: kratos.api.AdminBackend.Storage.cloudflare_r2:type_name -> kratos.api.AdminBackend.ObjectStore
14, // 25: kratos.api.AdminBackend.Storage.minio:type_name -> kratos.api.AdminBackend.ObjectStore
26, // [26:26] is the sub-list for method output_type
26, // [26:26] is the sub-list for method input_type
26, // [26:26] is the sub-list for extension type_name
26, // [26:26] is the sub-list for extension extendee
0, // [0:26] is the sub-list for field type_name
}
func init() { file_conf_conf_proto_init() }
@ -922,7 +1364,7 @@ func file_conf_conf_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_conf_conf_proto_rawDesc), len(file_conf_conf_proto_rawDesc)),
NumEnums: 0,
NumMessages: 12,
NumMessages: 15,
NumExtensions: 0,
NumServices: 0,
},

View File

@ -30,6 +30,13 @@ message Data {
message Database {
string driver = 1;
string source = 2;
string host = 3;
string port = 4;
string user = 5;
string password = 6;
string name = 7;
string config = 8;
string path = 9;
}
message Redis {
string network = 1;
@ -48,6 +55,9 @@ message AdminBackend {
Captcha captcha = 3;
Local local = 4;
Email email = 5;
Storage storage = 6;
// ConfigPath is populated by the entrypoint and is not required in YAML.
string config_path = 7;
message JWT {
string signing_key = 1;
@ -78,4 +88,40 @@ message AdminBackend {
bool is_ssl = 7;
bool is_login_auth = 8;
}
message Storage {
string type = 1;
Qiniu qiniu = 2;
ObjectStore aliyun_oss = 3;
ObjectStore huawei_obs = 4;
ObjectStore tencent_cos = 5;
ObjectStore aws_s3 = 6;
ObjectStore cloudflare_r2 = 7;
ObjectStore minio = 8;
}
message Qiniu {
string zone = 1;
string bucket = 2;
string base_url = 3;
string access_key = 4;
string secret_key = 5;
bool use_https = 6;
bool use_cdn_domains = 7;
}
// ObjectStore covers S3-compatible configuration used by public cloud
// providers and self-hosted MinIO.
message ObjectStore {
string endpoint = 1;
string region = 2;
string bucket = 3;
string access_key = 4;
string secret_key = 5;
string base_url = 6;
string path_prefix = 7;
bool use_ssl = 8;
bool force_path_style = 9;
string account_id = 10;
}
}

View File

@ -2,126 +2,84 @@ package data
import (
"context"
"errors"
"time"
"github.com/go-kratos/aip-go/ents"
"gorm.io/gorm"
"kra/internal/biz"
"kra/internal/data/ent"
"kra/internal/data/ent/admin"
)
func convertAdmin(po *ent.Admin) *biz.Admin {
return &biz.Admin{
ID: po.ID,
Name: po.Name,
Email: po.Email,
Avatar: po.Avatar,
Access: po.Access,
Password: po.Password,
CreateTime: po.CreateTime,
UpdateTime: po.UpdateTime,
type adminPO struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"uniqueIndex"`
Email string `gorm:"uniqueIndex"`
Password string
Access string
Avatar string
CreateTime time.Time
UpdateTime time.Time
}
func (adminPO) TableName() string { return "admins" }
func convertAdmin(po *adminPO) *biz.Admin {
return &biz.Admin{ID: po.ID, Name: po.Name, Email: po.Email, Password: po.Password, Access: po.Access, Avatar: po.Avatar, CreateTime: po.CreateTime, UpdateTime: po.UpdateTime}
}
type adminRepo struct{ data *Data }
func NewAdminRepo(data *Data) biz.AdminRepo { return &adminRepo{data: data} }
func (r *adminRepo) one(ctx context.Context, query *gorm.DB) (*biz.Admin, error) {
var po adminPO
if err := query.WithContext(ctx).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrAdminNotFound
}
return nil, err
}
return convertAdmin(&po), nil
}
type adminRepo struct {
data *Data
}
// NewAdminRepo creates a new AdminRepo instance.
func NewAdminRepo(data *Data) biz.AdminRepo {
return &adminRepo{
data: data,
}
}
func (r *adminRepo) FindByID(ctx context.Context, id int64) (*biz.Admin, error) {
po, err := r.data.db.Admin.Get(ctx, id)
if err != nil {
if ent.IsNotFound(err) {
return nil, biz.ErrAdminNotFound
}
return nil, err
}
return convertAdmin(po), nil
return r.one(ctx, r.data.gormDB.Where("id = ?", id))
}
func (r *adminRepo) FindByName(ctx context.Context, name string) (*biz.Admin, error) {
po, err := r.data.db.Admin.Query().Where(admin.NameEQ(name)).Only(ctx)
if err != nil {
if ent.IsNotFound(err) {
return nil, biz.ErrAdminNotFound
}
return nil, err
}
return convertAdmin(po), nil
return r.one(ctx, r.data.gormDB.Where("name = ?", name))
}
func (r *adminRepo) FindByEmail(ctx context.Context, email string) (*biz.Admin, error) {
po, err := r.data.db.Admin.Query().Where(admin.EmailEQ(email)).Only(ctx)
if err != nil {
if ent.IsNotFound(err) {
return nil, biz.ErrAdminNotFound
}
return nil, err
}
return convertAdmin(po), nil
return r.one(ctx, r.data.gormDB.Where("email = ?", email))
}
func (r *adminRepo) ListAdmins(ctx context.Context, opts ...biz.ListOption) ([]*biz.Admin, error) {
o := biz.ListOptions{Limit: 20}
for _, opt := range opts {
opt(&o)
}
pos, err := r.data.db.Admin.Query().
Where(ents.ApplyFilter(o.Filter)).
Order(ents.ApplyOrderBy(o.OrderBy)).
Offset(o.Offset).
Limit(o.Limit).
All(ctx)
if err != nil {
var pos []adminPO
if err := r.data.gormDB.WithContext(ctx).Offset(o.Offset).Limit(o.Limit).Order("id asc").Find(&pos).Error; err != nil {
return nil, err
}
var admins []*biz.Admin
for _, po := range pos {
admins = append(admins, convertAdmin(po))
out := make([]*biz.Admin, 0, len(pos))
for i := range pos {
out = append(out, convertAdmin(&pos[i]))
}
return admins, nil
return out, nil
}
func (r *adminRepo) CreateAdmin(ctx context.Context, admin *biz.Admin) (*biz.Admin, error) {
po, err := r.data.db.Admin.Create().
SetName(admin.Name).
SetEmail(admin.Email).
SetAvatar(admin.Avatar).
SetAccess(admin.Access).
SetPassword(admin.Password).
SetCreateTime(time.Now()).
SetUpdateTime(time.Now()).
Save(ctx)
if err != nil {
func (r *adminRepo) CreateAdmin(ctx context.Context, a *biz.Admin) (*biz.Admin, error) {
now := time.Now()
po := adminPO{Name: a.Name, Email: a.Email, Password: a.Password, Access: a.Access, Avatar: a.Avatar, CreateTime: now, UpdateTime: now}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
return nil, err
}
return convertAdmin(po), nil
return convertAdmin(&po), nil
}
func (r *adminRepo) UpdateAdmin(ctx context.Context, admin *biz.Admin) (*biz.Admin, error) {
update := r.data.db.Admin.UpdateOneID(admin.ID).
SetName(admin.Name).
SetEmail(admin.Email).
SetAccess(admin.Access).
SetAvatar(admin.Avatar).
SetUpdateTime(time.Now())
// Only update the password if it's not empty
if admin.Password != "" {
update.SetPassword(admin.Password)
func (r *adminRepo) UpdateAdmin(ctx context.Context, a *biz.Admin) (*biz.Admin, error) {
values := map[string]any{"name": a.Name, "email": a.Email, "access": a.Access, "avatar": a.Avatar, "update_time": time.Now()}
if a.Password != "" {
values["password"] = a.Password
}
po, err := update.Save(ctx)
if err != nil {
if err := r.data.gormDB.WithContext(ctx).Model(&adminPO{}).Where("id = ?", a.ID).Updates(values).Error; err != nil {
return nil, err
}
return convertAdmin(po), nil
return r.FindByID(ctx, a.ID)
}
func (r *adminRepo) DeleteAdmin(ctx context.Context, id int64) error {
return r.data.db.Admin.DeleteOneID(id).Exec(ctx)
return r.data.gormDB.WithContext(ctx).Delete(&adminPO{}, id).Error
}

View File

@ -0,0 +1,103 @@
package data
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"gopkg.in/yaml.v3"
)
func protoMap(message proto.Message) (map[string]any, error) {
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(message)
if err != nil {
return nil, err
}
var value map[string]any
if err = json.Unmarshal(raw, &value); err != nil {
return nil, err
}
delete(value, "config_path")
return value, nil
}
func setYAMLMapping(node *yaml.Node, key string, value any) error {
if node.Kind == yaml.DocumentNode {
node = node.Content[0]
}
if node.Kind != yaml.MappingNode {
return fmt.Errorf("configuration root is not a mapping")
}
raw, err := yaml.Marshal(value)
if err != nil {
return err
}
var replacement yaml.Node
if err = yaml.Unmarshal(raw, &replacement); err != nil {
return err
}
for i := 0; i < len(node.Content); i += 2 {
if node.Content[i].Value == key {
node.Content[i+1] = replacement.Content[0]
return nil
}
}
node.Content = append(node.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: key}, replacement.Content[0])
return nil
}
func (d *Data) persistConfig() error {
if d.admin == nil || d.admin.ConfigPath == "" {
return nil
}
raw, err := os.ReadFile(d.admin.ConfigPath)
if err != nil {
return err
}
var document yaml.Node
if err = yaml.Unmarshal(raw, &document); err != nil {
return err
}
dataValue, err := protoMap(d.config)
if err != nil {
return err
}
adminValue, err := protoMap(d.admin)
if err != nil {
return err
}
if err = setYAMLMapping(&document, "data", dataValue); err != nil {
return err
}
if err = setYAMLMapping(&document, "admin", adminValue); err != nil {
return err
}
output, err := yaml.Marshal(&document)
if err != nil {
return err
}
if err = os.MkdirAll(filepath.Dir(d.admin.ConfigPath), 0o755); err != nil {
return err
}
temporary, err := os.CreateTemp(filepath.Dir(d.admin.ConfigPath), ".kra-config-*.yaml")
if err != nil {
return err
}
tempName := temporary.Name()
defer os.Remove(tempName)
if _, err = temporary.Write(output); err != nil {
_ = temporary.Close()
return err
}
if err = temporary.Chmod(0o600); err != nil {
_ = temporary.Close()
return err
}
if err = temporary.Close(); err != nil {
return err
}
return os.Rename(tempName, d.admin.ConfigPath)
}

View File

@ -4,55 +4,47 @@ import (
"context"
"fmt"
"log"
"os"
"sync"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/google/wire"
"github.com/redis/go-redis/v9"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"kra/internal/conf"
"kra/internal/data/ent"
"kra/internal/data/ent/migrate"
)
// ProviderSet is data providers.
var ProviderSet = wire.NewSet(NewData, NewAdminRepo, NewSystemRepo, NewAccessRepo, NewSettingsRepo, NewVersionRepo, NewExportRepo, NewAuditRepo, NewTaskRepo, NewMediaRepo, NewAnnouncementRepo, NewEmailRepo, NewCache, NewFileStorage)
// Data is a struct that contains the database client.
type Data struct {
db *ent.Client
gormDB *gorm.DB
redis *redis.Client
mu sync.RWMutex
gormDB *gorm.DB
redis *redis.Client
database *conf.Data_Database
config *conf.Data
admin *conf.AdminBackend
}
// NewData creates a new Data instance.
func NewData(c *conf.Data) (*Data, func(), error) {
func NewData(c *conf.Data, admin *conf.AdminBackend) (*Data, func(), error) {
if c == nil || c.Database == nil {
return nil, nil, fmt.Errorf("database configuration is required")
}
db, err := ent.Open(c.Database.Driver, c.Database.Source)
d := &Data{database: c.Database, config: c, admin: admin}
db, err := openDatabase(c.Database, false)
if err != nil {
return nil, nil, fmt.Errorf("open ent database: %w", err)
}
gormDB, err := gorm.Open(mysql.Open(c.Database.Source), &gorm.Config{})
if err != nil {
_ = db.Close()
return nil, nil, err
}
// The announcement module owns its table lifecycle independently of the
// first-run database initializer.
if err = gormDB.AutoMigrate(&announcementPO{}); err != nil {
_ = db.Close()
return nil, nil, fmt.Errorf("migrate announcement table: %w", err)
}
var redisClient *redis.Client
if c.Redis != nil && c.Redis.Addr != "" {
options := &redis.Options{Addr: c.Redis.Addr}
if c.Redis.Network != "" {
options.Network = c.Redis.Network
// The initialization endpoint must remain available when the configured
// target database has not been created yet.
log.Printf("configured database unavailable before initialization: %v", err)
db, err = openFallbackDatabase()
if err != nil {
return nil, nil, fmt.Errorf("open bootstrap database: %w", err)
}
}
d.gormDB = db
if err = db.AutoMigrate(&announcementPO{}, &adminPO{}); err != nil {
return nil, nil, fmt.Errorf("migrate bootstrap tables: %w", err)
}
if c.Redis != nil && c.Redis.Addr != "" {
options := &redis.Options{Addr: c.Redis.Addr, Network: c.Redis.Network}
if c.Redis.ReadTimeout != nil {
options.ReadTimeout = c.Redis.ReadTimeout.AsDuration()
}
@ -65,27 +57,42 @@ func NewData(c *conf.Data) (*Data, func(), error) {
log.Printf("redis unavailable, using in-memory cache: %v", pingErr)
_ = candidate.Close()
} else {
redisClient = candidate
d.redis = candidate
}
cancel()
}
if os.Getenv("DEPLOY_ENV") == "dev" {
// Enable debug mode for detailed logging.
db = db.Debug()
// Run the auto migration tool.
if err = db.Schema.Create(context.Background(), migrate.WithDropIndex(true)); err != nil {
return nil, nil, err
}
}
cleanup := func() {
_ = db.Close()
if redisClient != nil {
_ = redisClient.Close()
d.mu.RLock()
db := d.gormDB
d.mu.RUnlock()
if sqlDB, closeErr := db.DB(); closeErr == nil {
_ = sqlDB.Close()
}
if d.redis != nil {
_ = d.redis.Close()
}
}
return &Data{
db: db,
gormDB: gormDB,
redis: redisClient,
}, cleanup, nil
return d, cleanup, nil
}
func (d *Data) switchDatabase(config *conf.Data_Database) error {
db, err := openDatabase(config, true)
if err != nil {
return err
}
d.mu.Lock()
old := d.gormDB
d.gormDB = db
d.database = config
d.config.Database = config
d.mu.Unlock()
if err := d.persistConfig(); err != nil {
return fmt.Errorf("persist database configuration: %w", err)
}
if old != nil {
if sqlDB, e := old.DB(); e == nil {
_ = sqlDB.Close()
}
}
return nil
}

179
internal/data/database.go Normal file
View File

@ -0,0 +1,179 @@
package data
import (
"fmt"
"net"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
oracle "github.com/dzwvip/gorm-oracle"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
"kra/internal/conf"
)
var databaseNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_$-]*$`)
func normalizedDriver(driver string) string {
switch strings.ToLower(strings.TrimSpace(driver)) {
case "postgres", "postgresql", "pgsql":
return "pgsql"
case "sqlserver", "mssql":
return "mssql"
case "sqlite", "sqlite3":
return "sqlite"
case "oracle":
return "oracle"
default:
return "mysql"
}
}
func databaseDSN(c *conf.Data_Database, name string) (string, error) {
if c.Source != "" && c.Host == "" && c.Path == "" {
return c.Source, nil
}
driver := normalizedDriver(c.Driver)
if name == "" {
name = c.Name
}
host := c.Host
if host == "" {
host = "127.0.0.1"
}
switch driver {
case "mysql":
port := c.Port
if port == "" {
port = "3306"
}
query := c.Config
if query == "" {
query = "timeout=5s&parseTime=True&loc=Local&charset=utf8mb4"
}
return fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", c.User, c.Password, net.JoinHostPort(host, port), name, query), nil
case "pgsql":
port := c.Port
if port == "" {
port = "5432"
}
extra := c.Config
if extra == "" {
extra = "sslmode=disable TimeZone=Local"
}
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s %s", host, port, c.User, c.Password, name, extra), nil
case "mssql":
port := c.Port
if port == "" {
port = "1433"
}
q := url.Values{"database": {name}, "encrypt": {"disable"}}
return (&url.URL{Scheme: "sqlserver", User: url.UserPassword(c.User, c.Password), Host: net.JoinHostPort(host, port), RawQuery: q.Encode()}).String(), nil
case "oracle":
port := c.Port
if port == "" {
port = "1521"
}
return fmt.Sprintf("oracle://%s:%s@%s/%s?%s", url.PathEscape(c.User), url.PathEscape(c.Password), net.JoinHostPort(host, port), url.PathEscape(name), c.Config), nil
case "sqlite":
path := c.Path
if path == "" {
path = "."
}
if name == "" {
name = "kra"
}
if filepath.Ext(name) == "" {
name += ".db"
}
return filepath.Join(path, name), nil
}
return "", fmt.Errorf("unsupported database driver %q", c.Driver)
}
func openWithDriver(driver, dsn string) (*gorm.DB, error) {
switch normalizedDriver(driver) {
case "mysql":
return gorm.Open(mysql.Open(dsn), &gorm.Config{})
case "pgsql":
return gorm.Open(postgres.Open(dsn), &gorm.Config{})
case "mssql":
return gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
case "oracle":
return gorm.Open(oracle.Open(dsn), &gorm.Config{})
case "sqlite":
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
default:
return nil, fmt.Errorf("unsupported database driver %q", driver)
}
}
func openDatabase(c *conf.Data_Database, create bool) (*gorm.DB, error) {
driver := normalizedDriver(c.Driver)
if driver == "sqlite" {
dsn, err := databaseDSN(c, "")
if err != nil {
return nil, err
}
if err = os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil {
return nil, err
}
return openWithDriver(driver, dsn)
}
if create && driver != "oracle" {
if !databaseNamePattern.MatchString(c.Name) {
return nil, fmt.Errorf("invalid database name %q", c.Name)
}
bootstrap := ""
switch driver {
case "pgsql":
bootstrap = "postgres"
case "mssql":
bootstrap = "master"
}
dsn, err := databaseDSN(c, bootstrap)
if err != nil {
return nil, err
}
adminDB, err := openWithDriver(driver, dsn)
if err != nil {
return nil, fmt.Errorf("connect database server: %w", err)
}
var statement string
switch driver {
case "mysql":
statement = "CREATE DATABASE IF NOT EXISTS `" + c.Name + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"
case "pgsql":
var count int64
if err = adminDB.Raw("SELECT count(*) FROM pg_database WHERE datname = ?", c.Name).Scan(&count).Error; err == nil && count == 0 {
statement = `CREATE DATABASE "` + c.Name + `"`
}
case "mssql":
statement = "IF DB_ID(N'" + c.Name + "') IS NULL CREATE DATABASE [" + c.Name + "]"
}
if statement != "" {
err = adminDB.Exec(statement).Error
}
if sqlDB, e := adminDB.DB(); e == nil {
_ = sqlDB.Close()
}
if err != nil {
return nil, fmt.Errorf("create database: %w", err)
}
}
dsn, err := databaseDSN(c, "")
if err != nil {
return nil, err
}
return openWithDriver(driver, dsn)
}
func openFallbackDatabase() (*gorm.DB, error) {
return openWithDriver("sqlite", "file:kra-bootstrap?mode=memory&cache=shared")
}

View File

@ -21,6 +21,33 @@ type fileStorage struct {
}
func NewFileStorage(config *conf.AdminBackend) (biz.FileStorage, error) {
storageType := "local"
if config != nil && config.Storage != nil && config.Storage.Type != "" {
storageType = strings.ToLower(config.Storage.Type)
}
if storageType == "qiniu" {
return newQiniuStorage(config.Storage.Qiniu)
}
if storageType != "local" {
var object *conf.AdminBackend_ObjectStore
switch storageType {
case "aliyun-oss":
object = config.Storage.AliyunOss
case "huawei-obs":
object = config.Storage.HuaweiObs
case "tencent-cos":
object = config.Storage.TencentCos
case "aws-s3":
object = config.Storage.AwsS3
case "cloudflare-r2":
object = config.Storage.CloudflareR2
case "minio":
object = config.Storage.Minio
default:
return nil, fmt.Errorf("unsupported storage type %q", storageType)
}
return newS3Storage(storageType, object)
}
root, prefix := "uploads/file", "uploads/file"
if config != nil && config.Local != nil {
if config.Local.StorePath != "" {
@ -37,6 +64,43 @@ func NewFileStorage(config *conf.AdminBackend) (biz.FileStorage, error) {
return &fileStorage{root: root, urlPrefix: "/" + strings.Trim(prefix, "/")}, nil
}
func composeFiles(ctx context.Context, storage biz.FileStorage, names []string, destination string) (*biz.StoredFile, string, error) {
reader, writer := io.Pipe()
hash := md5.New()
errCh := make(chan error, 1)
go func() {
defer writer.Close()
for _, name := range names {
if ctx.Err() != nil {
errCh <- ctx.Err()
return
}
file, err := storage.Open(ctx, name)
if err != nil {
errCh <- err
return
}
_, err = io.Copy(io.MultiWriter(writer, hash), file)
_ = file.Close()
if err != nil {
errCh <- err
return
}
}
errCh <- nil
}()
stored, err := storage.Put(ctx, destination, reader)
composeErr := <-errCh
if err != nil {
return nil, "", err
}
if composeErr != nil {
_ = storage.Delete(ctx, destination)
return nil, "", composeErr
}
return stored, hex.EncodeToString(hash.Sum(nil)), nil
}
func (s *fileStorage) resolve(name string) (string, error) {
clean := filepath.Clean(strings.TrimPrefix(name, "/"))
if clean == "." || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
@ -88,40 +152,7 @@ func (s *fileStorage) Delete(ctx context.Context, name string) error {
}
func (s *fileStorage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) {
reader, writer := io.Pipe()
hash := md5.New()
errCh := make(chan error, 1)
go func() {
defer writer.Close()
for _, name := range names {
if ctx.Err() != nil {
errCh <- ctx.Err()
return
}
file, err := s.Open(ctx, name)
if err != nil {
errCh <- err
return
}
_, err = io.Copy(io.MultiWriter(writer, hash), file)
_ = file.Close()
if err != nil {
errCh <- err
return
}
}
errCh <- nil
}()
stored, err := s.Put(ctx, destination, reader)
composeErr := <-errCh
if err != nil {
return nil, "", err
}
if composeErr != nil {
_ = s.Delete(ctx, destination)
return nil, "", composeErr
}
return stored, hex.EncodeToString(hash.Sum(nil)), nil
return composeFiles(ctx, s, names, destination)
}
func (s *fileStorage) DeletePrefix(ctx context.Context, prefix string) error {
path, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder")

View File

@ -143,7 +143,7 @@ func (r *mediaRepo) DeleteMedia(ctx context.Context, id uint) error {
}
func (r *mediaRepo) MediaKeyReferences(ctx context.Context, key string) (int64, error) {
var count int64
err := r.data.gormDB.WithContext(ctx).Model(&mediaPO{}).Where("`key` = ?", key).Count(&count).Error
err := r.data.gormDB.WithContext(ctx).Model(&mediaPO{}).Where(map[string]any{"key": key}).Count(&count).Error
return count, err
}
func (r *mediaRepo) CreateMediaBatch(ctx context.Context, items []*biz.MediaFile) error {

View File

@ -0,0 +1,126 @@
package data
import (
"context"
"fmt"
"io"
"net/http"
"os"
"path"
"strings"
"time"
"github.com/qiniu/go-sdk/v7/auth/qbox"
qstorage "github.com/qiniu/go-sdk/v7/storage"
"kra/internal/biz"
"kra/internal/conf"
)
type qiniuStorage struct {
config *conf.AdminBackend_Qiniu
upload *qstorage.FormUploader
manager *qstorage.BucketManager
mac *qbox.Mac
token string
}
func newQiniuStorage(config *conf.AdminBackend_Qiniu) (biz.FileStorage, error) {
if config == nil || config.Bucket == "" || config.AccessKey == "" || config.SecretKey == "" {
return nil, fmt.Errorf("qiniu storage configuration is incomplete")
}
cfg := qstorage.Config{UseHTTPS: config.UseHttps, UseCdnDomains: config.UseCdnDomains}
switch config.Zone {
case "ZoneHuadong":
cfg.Zone = &qstorage.ZoneHuadong
case "ZoneHuabei":
cfg.Zone = &qstorage.ZoneHuabei
case "ZoneHuanan":
cfg.Zone = &qstorage.ZoneHuanan
case "ZoneBeimei":
cfg.Zone = &qstorage.ZoneBeimei
case "ZoneXinjiapo":
cfg.Zone = &qstorage.ZoneXinjiapo
}
mac := qbox.NewMac(config.AccessKey, config.SecretKey)
policy := qstorage.PutPolicy{Scope: config.Bucket}
token := policy.UploadToken(mac)
return &qiniuStorage{config: config, upload: qstorage.NewFormUploader(&cfg), manager: qstorage.NewBucketManager(mac, &cfg), mac: mac, token: token}, nil
}
func (s *qiniuStorage) file(key string, size int64) *biz.StoredFile {
return &biz.StoredFile{Name: path.Base(key), Path: key, URL: strings.TrimSuffix(s.config.BaseUrl, "/") + "/" + key, Size: size}
}
func (s *qiniuStorage) Put(ctx context.Context, name string, reader io.Reader) (*biz.StoredFile, error) {
temporary, err := os.CreateTemp("", "kra-qiniu-upload-*")
if err != nil {
return nil, err
}
temporaryName := temporary.Name()
defer os.Remove(temporaryName)
size, err := io.Copy(temporary, reader)
if err != nil {
_ = temporary.Close()
return nil, err
}
if _, err = temporary.Seek(0, io.SeekStart); err != nil {
_ = temporary.Close()
return nil, err
}
defer temporary.Close()
ret := qstorage.PutRet{}
if err = s.upload.Put(ctx, &ret, s.token, strings.TrimPrefix(name, "/"), temporary, size, &qstorage.PutExtra{}); err != nil {
return nil, err
}
return s.file(ret.Key, size), nil
}
func (s *qiniuStorage) Open(ctx context.Context, name string) (io.ReadCloser, error) {
downloadURL := qstorage.MakePrivateURL(s.mac, strings.TrimSuffix(s.config.BaseUrl, "/"), strings.TrimPrefix(name, "/"), time.Now().Add(time.Hour).Unix())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode >= 300 {
_ = resp.Body.Close()
return nil, fmt.Errorf("qiniu download failed: %s", resp.Status)
}
return resp.Body, nil
}
func (s *qiniuStorage) Delete(ctx context.Context, name string) error {
return s.manager.Delete(s.config.Bucket, strings.TrimPrefix(name, "/"))
}
func (s *qiniuStorage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) {
return composeFiles(ctx, s, names, destination)
}
func (s *qiniuStorage) DeletePrefix(ctx context.Context, prefix string) error {
for {
items, _, more, err := s.List(ctx, prefix, "", 1000)
if err != nil {
return err
}
for _, item := range items {
if err = s.Delete(ctx, item.Path); err != nil {
return err
}
}
if !more {
return nil
}
}
}
func (s *qiniuStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
if limit < 1 || limit > 1000 {
limit = 100
}
entries, _, marker, more, err := s.manager.ListFiles(s.config.Bucket, prefix, "", cursor, limit)
if err != nil {
return nil, "", false, err
}
out := make([]*biz.StoredFile, 0, len(entries))
for _, entry := range entries {
out = append(out, s.file(entry.Key, entry.Fsize))
}
return out, marker, more, nil
}

138
internal/data/s3_storage.go Normal file
View File

@ -0,0 +1,138 @@
package data
import (
"context"
"fmt"
"io"
"net/url"
"path"
"strings"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"kra/internal/biz"
"kra/internal/conf"
)
type s3Storage struct {
client *minio.Client
bucket, baseURL, prefix string
}
func newS3Storage(provider string, config *conf.AdminBackend_ObjectStore) (biz.FileStorage, error) {
if config == nil || config.Bucket == "" || config.AccessKey == "" || config.SecretKey == "" {
return nil, fmt.Errorf("%s storage configuration is incomplete", provider)
}
endpoint := config.Endpoint
if endpoint == "" {
switch provider {
case "aws-s3":
endpoint = "s3." + config.Region + ".amazonaws.com"
case "cloudflare-r2":
endpoint = config.AccountId + ".r2.cloudflarestorage.com"
case "aliyun-oss":
endpoint = "oss-" + config.Region + ".aliyuncs.com"
case "huawei-obs":
endpoint = "obs." + config.Region + ".myhuaweicloud.com"
case "tencent-cos":
endpoint = "cos." + config.Region + ".myqcloud.com"
}
}
secure := config.UseSsl
if parsed, err := url.Parse(endpoint); err == nil && parsed.Host != "" {
secure = parsed.Scheme == "https"
endpoint = parsed.Host
}
if endpoint == "" {
return nil, fmt.Errorf("%s endpoint is required", provider)
}
client, err := minio.New(strings.TrimSuffix(endpoint, "/"), &minio.Options{Creds: credentials.NewStaticV4(config.AccessKey, config.SecretKey, ""), Secure: secure, Region: config.Region, BucketLookup: func() minio.BucketLookupType {
if config.ForcePathStyle {
return minio.BucketLookupPath
}
return minio.BucketLookupAuto
}()})
if err != nil {
return nil, err
}
return &s3Storage{client: client, bucket: config.Bucket, baseURL: strings.TrimSuffix(config.BaseUrl, "/"), prefix: strings.Trim(config.PathPrefix, "/")}, nil
}
func (s *s3Storage) key(name string) string {
if s.prefix == "" {
return strings.TrimPrefix(name, "/")
}
return path.Join(s.prefix, strings.TrimPrefix(name, "/"))
}
func (s *s3Storage) unkey(key string) string {
return strings.TrimPrefix(strings.TrimPrefix(key, s.prefix), "/")
}
func (s *s3Storage) file(key string, size int64) *biz.StoredFile {
name := s.unkey(key)
rawURL := s.baseURL + "/" + key
if s.baseURL == "" {
rawURL = key
}
return &biz.StoredFile{Name: path.Base(name), Path: name, URL: rawURL, Size: size}
}
func (s *s3Storage) Put(ctx context.Context, name string, reader io.Reader) (*biz.StoredFile, error) {
key := s.key(name)
info, err := s.client.PutObject(ctx, s.bucket, key, reader, -1, minio.PutObjectOptions{})
if err != nil {
return nil, err
}
return s.file(key, info.Size), nil
}
func (s *s3Storage) Open(ctx context.Context, name string) (io.ReadCloser, error) {
obj, err := s.client.GetObject(ctx, s.bucket, s.key(name), minio.GetObjectOptions{})
if err != nil {
return nil, err
}
if _, err = obj.Stat(); err != nil {
_ = obj.Close()
return nil, err
}
return obj, nil
}
func (s *s3Storage) Delete(ctx context.Context, name string) error {
return s.client.RemoveObject(ctx, s.bucket, s.key(name), minio.RemoveObjectOptions{})
}
func (s *s3Storage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) {
return composeFiles(ctx, s, names, destination)
}
func (s *s3Storage) DeletePrefix(ctx context.Context, prefix string) error {
items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: s.key(prefix), Recursive: true})
for item := range items {
if item.Err != nil {
return item.Err
}
if err := s.client.RemoveObject(ctx, s.bucket, item.Key, minio.RemoveObjectOptions{}); err != nil {
return err
}
}
return nil
}
func (s *s3Storage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
if limit < 1 || limit > 1000 {
limit = 100
}
items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: s.key(prefix), Recursive: true, StartAfter: s.key(cursor)})
out := make([]*biz.StoredFile, 0, limit+1)
for item := range items {
if item.Err != nil {
return nil, "", false, item.Err
}
out = append(out, s.file(item.Key, item.Size))
if len(out) > limit {
break
}
}
more := len(out) > limit
if more {
out = out[:limit]
}
next := ""
if len(out) > 0 {
next = out[len(out)-1].Path
}
return out, next, more, nil
}

View File

@ -10,6 +10,7 @@ import (
"kra/internal/biz"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type dictionaryPO struct {
@ -334,7 +335,7 @@ func (r *settingsRepo) FindParameter(ctx context.Context, id uint, key string) (
if id != 0 {
err = db.First(&po, id).Error
} else {
err = db.Where("`key` = ?", key).First(&po).Error
err = db.Where(map[string]any{"key": key}).First(&po).Error
}
if err != nil {
return nil, err
@ -354,7 +355,7 @@ func (r *settingsRepo) ListParameters(ctx context.Context, page, size int, q *bi
db = db.Where("name LIKE ?", "%"+q.Name+"%")
}
if q.Key != "" {
db = db.Where("`key` LIKE ?", "%"+q.Key+"%")
db = db.Where(clause.Like{Column: clause.Column{Name: "key"}, Value: "%" + q.Key + "%"})
}
}
var total int64

View File

@ -7,6 +7,7 @@ import (
"time"
"kra/internal/biz"
"kra/internal/conf"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
@ -87,14 +88,22 @@ func (authorityMenuPO) TableName() string { return "sys_authority_menus" }
type systemRepo struct{ data *Data }
func NewSystemRepo(data *Data) biz.SystemRepo { return &systemRepo{data: data} }
func NewSystemRepo(data *Data) biz.SystemRepo { return &systemRepo{data: data} }
func (r *systemRepo) PersistConfig(context.Context) error { return r.data.persistConfig() }
func (r *systemRepo) IsInitialized(ctx context.Context) (bool, error) {
return r.data.gormDB.WithContext(ctx).Migrator().HasTable(&userPO{}), nil
}
func (r *systemRepo) Initialize(ctx context.Context) error {
func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error {
config := &conf.Data_Database{Driver: input.Driver, Host: input.Host, Port: input.Port, User: input.User, Password: input.Password, Name: input.Name, Path: input.Path, Config: input.Config}
if err := r.data.switchDatabase(config); err != nil {
return err
}
db := r.data.gormDB.WithContext(ctx)
if err := db.AutoMigrate(&adminPO{}); err != nil {
return err
}
if err := db.AutoMigrate(&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &menuButtonPO{}, &authorityButtonPO{}, &departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{}, &dictionaryPO{}, &dictionaryDetailPO{}, &parameterPO{}, &apiTokenPO{}, &securityConfigPO{}, &versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{}, &operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{}, &taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{}, &announcementPO{}); err != nil {
return err
}
@ -140,7 +149,7 @@ func (r *systemRepo) Initialize(ctx context.Context) error {
return err
}
if count == 0 {
hash, err := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
hash, err := bcrypt.GenerateFromPassword([]byte(input.AdminPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
@ -349,9 +358,13 @@ func (r *systemRepo) DeleteUser(ctx context.Context, id uint) error {
return tx.Delete(&userPO{}, id).Error
})
}
func (r *systemRepo) UpdatePassword(ctx context.Context, id uint, password string) error {
func (r *systemRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error {
now := time.Now()
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(map[string]any{"password": password, "password_updated_at": now, "must_change_password": false}).Error
updates := map[string]any{"password": password, "password_updated_at": now}
if clearMustChange {
updates["must_change_password"] = false
}
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(updates).Error
}
func (r *systemRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
var pos []authorityPO

View File

@ -62,7 +62,7 @@ func redactJSON(raw []byte) string {
}
func operationAudit(svc *service.AuditService) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
if isBootstrapPath(c.Request.URL.Path) || c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
c.Next()
return
}

View File

@ -34,7 +34,8 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.Use(ginRequestMeta(), emailErrorAlert(emails, logger), gin.Recovery(), securityRateLimit(svc, settings), operationAudit(audit))
if config != nil && config.Local != nil && config.Local.StorePath != "" {
localStorage := config == nil || config.Storage == nil || config.Storage.Type == "" || config.Storage.Type == "local"
if localStorage && config != nil && config.Local != nil && config.Local.StorePath != "" {
pathPrefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
if pathPrefix != "/" {
engine.StaticFS(pathPrefix, filesOnly{FileSystem: http.Dir(config.Local.StorePath)})
@ -51,11 +52,11 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System
})
registerPublicRoutes(public, engine, config, svc, settings, access, audit)
private := engine.Group(prefix)
private.Use(jwtAuth(config, settings), accessControl(access, audit))
private.Use(jwtAuth(config, settings), mustChangePasswordGuard(), accessControl(access, audit))
registerPrivateRoutes(private, svc)
registerAccessRoutes(private, engine, access)
registerSettingsRoutes(private, public, settings)
registerSystemConfigRoutes(private, config, settings)
registerSystemConfigRoutes(private, config, svc, settings)
registerVersionRoutes(private, versions)
registerExportRoutes(private, public, svc, exports)
registerAuditRoutes(private, public, audit)
@ -117,6 +118,22 @@ func currentClaims(c *gin.Context) *adminauth.Claims {
return result
}
func mustChangePasswordGuard() gin.HandlerFunc {
return func(c *gin.Context) {
claims := currentClaims(c)
if claims == nil || !claims.MustChangePwd {
c.Next()
return
}
path := strings.TrimSuffix(c.Request.URL.Path, "/")
if strings.HasSuffix(path, "/user/changePassword") || strings.HasSuffix(path, "/user/getUserInfo") || strings.HasSuffix(path, "/jwt/jsonInBlacklist") {
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusConflict, response{Code: codePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "请先修改初始密码"})
}
}
func registerPublicRoutes(group *gin.RouterGroup, engine *gin.Engine, config *conf.AdminBackend, svc *service.SystemService, settings *service.SettingsService, access *service.AccessService, audit *service.AuditService) {
expiration := 3 * time.Minute
keyLong, width, height := 6, 240, 80
@ -221,8 +238,13 @@ func registerPublicRoutes(group *gin.RouterGroup, engine *gin.Engine, config *co
writeResult(c, codeSuccess, gin.H{"needInit": !initialized}, message)
})
initGroup.POST("/initdb", func(c *gin.Context) {
if err := svc.Initialize(c.Request.Context()); err != nil {
fail(c, "自动创建数据库失败")
var input service.DatabaseInit
if err := c.ShouldBindJSON(&input); err != nil {
fail(c, "数据库初始化参数无效")
return
}
if err := svc.Initialize(c.Request.Context(), &input); err != nil {
fail(c, "自动创建数据库失败: "+err.Error())
return
}
routes := engine.Routes()
@ -261,6 +283,10 @@ func incrementCached(ctx context.Context, svc *service.SystemService, key string
}
func securityRateLimit(svc *service.SystemService, settings *service.SettingsService) gin.HandlerFunc {
return func(c *gin.Context) {
if isBootstrapPath(c.Request.URL.Path) {
c.Next()
return
}
cfg, err := settings.CurrentSecurity(c.Request.Context())
if err != nil || cfg == nil || !cfg.LimitEnable {
c.Next()
@ -281,6 +307,10 @@ func securityRateLimit(svc *service.SystemService, settings *service.SettingsSer
}
}
func isBootstrapPath(path string) bool {
return strings.HasSuffix(path, "/health") || strings.Contains(path, "/init/")
}
type captchaStore struct {
service *service.SystemService
expiration time.Duration

View File

@ -3,15 +3,19 @@ package server
import (
"runtime"
"syscall"
"time"
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/service"
"github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/mem"
"google.golang.org/protobuf/proto"
)
func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBackend, settings *service.SettingsService) {
func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBackend, systemService *service.SystemService, settings *service.SettingsService) {
security := group.Group("/securityConfig")
security.GET("/getSecurityConfig", func(c *gin.Context) {
value, err := settings.Security(c.Request.Context())
@ -53,6 +57,11 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
if config.Email != nil {
email = gin.H{"to": config.Email.To, "from": config.Email.From, "host": config.Email.Host, "secret": "******", "nickname": config.Email.Nickname, "port": config.Email.Port, "is-ssl": config.Email.IsSsl, "is-loginauth": config.Email.IsLoginAuth}
}
if config.Storage != nil {
storage := proto.Clone(config.Storage).(*conf.AdminBackend_Storage)
maskStorageSecrets(storage)
admin["storage"] = storage
}
}
writeResult(c, codeSuccess, gin.H{"config": gin.H{"admin": admin, "email": email}}, "获取成功")
})
@ -69,6 +78,7 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
StorePath string `json:"storePath"`
PathPrefix string `json:"pathPrefix"`
} `json:"local"`
Storage *conf.AdminBackend_Storage `json:"storage"`
} `json:"admin"`
Email *struct {
To string `json:"to"`
@ -104,6 +114,10 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
config.Local.PathPrefix = req.Config.Admin.Local.PathPrefix
}
}
if req.Config.Admin.Storage != nil {
preserveStorageSecrets(req.Config.Admin.Storage, config.Storage)
config.Storage = req.Config.Admin.Storage
}
if config.Email != nil && req.Config.Email != nil {
config.Email.To = req.Config.Email.To
config.Email.From = req.Config.Email.From
@ -117,12 +131,18 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
}
}
}
ok(c)
if err := systemService.PersistConfig(c.Request.Context()); err != nil {
fail(c, "配置保存失败: "+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{"restartRequired": true}, "配置已保存,重启服务后完整生效")
})
system.POST("/reloadSystem", func(c *gin.Context) {
writeResult(c, codeSuccess, gin.H{"reload": false, "restartRequired": true}, "配置已在当前进程部分生效;路由与存储配置需重启服务")
})
system.POST("/reloadSystem", func(c *gin.Context) { writeResult(c, codeSuccess, gin.H{"reload": true}, "配置已热更新") })
system.POST("/getServerInfo", func(c *gin.Context) {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
cpuPercent, _ := cpu.Percent(200*time.Millisecond, false)
memory, _ := mem.VirtualMemory()
disk := []gin.H{}
var stat syscall.Statfs_t
if syscall.Statfs(".", &stat) == nil {
@ -135,7 +155,42 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
}
disk = append(disk, gin.H{"mountPoint": ".", "usedMb": used / 1024 / 1024, "usedGb": used / 1024 / 1024 / 1024, "totalMb": total / 1024 / 1024, "totalGb": total / 1024 / 1024 / 1024, "usedPercent": percent})
}
server := gin.H{"os": gin.H{"goos": runtime.GOOS, "numCpu": runtime.NumCPU(), "compiler": runtime.Compiler, "goVersion": runtime.Version(), "numGoroutine": runtime.NumGoroutine()}, "cpu": gin.H{"cpus": []float64{}, "cores": runtime.NumCPU()}, "ram": gin.H{"usedMb": mem.Alloc / 1024 / 1024, "totalMb": mem.Sys / 1024 / 1024, "usedPercent": 0}, "disk": disk}
usedMB, totalMB, memoryPercent := uint64(0), uint64(0), float64(0)
if memory != nil {
usedMB, totalMB, memoryPercent = memory.Used/1024/1024, memory.Total/1024/1024, memory.UsedPercent
}
server := gin.H{"os": gin.H{"goos": runtime.GOOS, "numCpu": runtime.NumCPU(), "compiler": runtime.Compiler, "goVersion": runtime.Version(), "numGoroutine": runtime.NumGoroutine()}, "cpu": gin.H{"cpus": cpuPercent, "cores": runtime.NumCPU()}, "ram": gin.H{"usedMb": usedMB, "totalMb": totalMB, "usedPercent": memoryPercent}, "disk": disk}
writeResult(c, codeSuccess, gin.H{"server": server}, "获取成功")
})
}
func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_ObjectStore {
if storage == nil {
return nil
}
return []*conf.AdminBackend_ObjectStore{storage.AliyunOss, storage.HuaweiObs, storage.TencentCos, storage.AwsS3, storage.CloudflareR2, storage.Minio}
}
func maskStorageSecrets(storage *conf.AdminBackend_Storage) {
if storage.Qiniu != nil {
storage.Qiniu.SecretKey = "******"
}
for _, item := range objectStores(storage) {
if item != nil {
item.SecretKey = "******"
}
}
}
func preserveStorageSecrets(next, current *conf.AdminBackend_Storage) {
if next == nil || current == nil {
return
}
if next.Qiniu != nil && current.Qiniu != nil && (next.Qiniu.SecretKey == "" || next.Qiniu.SecretKey == "******") {
next.Qiniu.SecretKey = current.Qiniu.SecretKey
}
nextItems, currentItems := objectStores(next), objectStores(current)
for i := range nextItems {
if nextItems[i] != nil && currentItems[i] != nil && (nextItems[i].SecretKey == "" || nextItems[i].SecretKey == "******") {
nextItems[i].SecretKey = currentItems[i].SecretKey
}
}
}

View File

@ -29,7 +29,30 @@ func NewSystemService(uc *biz.SystemUsecase, config *conf.AdminBackend, settings
func (s *SystemService) IsInitialized(ctx context.Context) (bool, error) {
return s.uc.IsInitialized(ctx)
}
func (s *SystemService) Initialize(ctx context.Context) error { return s.uc.Initialize(ctx) }
func (s *SystemService) PersistConfig(ctx context.Context) error { return s.uc.PersistConfig(ctx) }
type DatabaseInit struct {
DBType string `json:"dbType"`
Host string `json:"host"`
Port string `json:"port"`
UserName string `json:"userName"`
Password string `json:"password"`
DBName string `json:"dbName"`
DBPath string `json:"dbPath"`
Template string `json:"template"`
AdminPassword string `json:"adminPassword"`
}
func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit) error {
config := ""
switch input.DBType {
case "mysql":
config = "timeout=5s&parseTime=True&loc=Local&charset=utf8mb4"
case "pgsql":
config = "sslmode=disable TimeZone=Local"
}
return s.uc.Initialize(ctx, &biz.DatabaseConfig{Driver: input.DBType, Host: input.Host, Port: input.Port, User: input.UserName, Password: input.Password, Name: input.DBName, Path: input.DBPath, Config: config, AdminPassword: input.AdminPassword})
}
func (s *SystemService) CacheGet(ctx context.Context, key string) (string, bool, error) {
return s.uc.CacheGet(ctx, key)

View File

@ -27,7 +27,6 @@
"axios": "1.8.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"echarts": "5.5.1",
"element-plus": "^2.13.6",
"highlight.js": "^11.10.0",
"mitt": "^3.0.1",
@ -43,7 +42,6 @@
"vite-auto-import-svg": "^2.9.8",
"vue": "^3.5.31",
"vue-cropper": "^1.1.4",
"vue-echarts": "^7.0.3",
"vue-qr": "^4.0.9",
"vue-router": "^4.4.3",
"vuedraggable": "^4.1.0"

View File

@ -53,9 +53,6 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
echarts:
specifier: 5.5.1
version: 5.5.1
element-plus:
specifier: ^2.13.6
version: 2.14.4(vue@3.5.41)
@ -101,9 +98,6 @@ importers:
vue-cropper:
specifier: ^1.1.4
version: 1.1.4
vue-echarts:
specifier: ^7.0.3
version: 7.0.3(@vue/runtime-core@3.5.41)(echarts@5.5.1)(vue@3.5.41)
vue-qr:
specifier: ^4.0.9
version: 4.0.9
@ -1604,9 +1598,6 @@ packages:
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
echarts@5.5.1:
resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==}
electron-to-chromium@1.5.405:
resolution: {integrity: sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==}
@ -2473,9 +2464,6 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@ -2621,17 +2609,6 @@ packages:
vue-cropper@1.1.4:
resolution: {integrity: sha512-5m98vBsCEI9rbS4JxELxXidtAui3qNyTHLHg67Qbn7g8cg+E6LcnC+hh3SM/p94x6mFh6KRxT1ttnta+wCYqWA==}
vue-demi@0.13.11:
resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'}
@ -2643,16 +2620,6 @@ packages:
'@vue/composition-api':
optional: true
vue-echarts@7.0.3:
resolution: {integrity: sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==}
peerDependencies:
'@vue/runtime-core': ^3.0.0
echarts: ^5.5.1
vue: ^2.7.0 || ^3.1.1
peerDependenciesMeta:
'@vue/runtime-core':
optional: true
vue-eslint-parser@9.4.3:
resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==}
engines: {node: ^14.17.0 || >=16.0.0}
@ -2710,9 +2677,6 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zrender@5.6.0:
resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==}
snapshots:
'@babel/code-frame@7.29.7':
@ -4295,11 +4259,6 @@ snapshots:
duplexer@0.1.2: {}
echarts@5.5.1:
dependencies:
tslib: 2.3.0
zrender: 5.6.0
electron-to-chromium@1.5.405: {}
element-plus@2.14.4(vue@3.5.41):
@ -5125,8 +5084,6 @@ snapshots:
totalist@3.0.1: {}
tslib@2.3.0: {}
tslib@2.8.1: {}
type-check@0.4.0:
@ -5258,24 +5215,10 @@ snapshots:
vue-cropper@1.1.4: {}
vue-demi@0.13.11(vue@3.5.41):
dependencies:
vue: 3.5.41
vue-demi@0.14.10(vue@3.5.41):
dependencies:
vue: 3.5.41
vue-echarts@7.0.3(@vue/runtime-core@3.5.41)(echarts@5.5.1)(vue@3.5.41):
dependencies:
echarts: 5.5.1
vue: 3.5.41
vue-demi: 0.13.11(vue@3.5.41)
optionalDependencies:
'@vue/runtime-core': 3.5.41
transitivePeerDependencies:
- '@vue/composition-api'
vue-eslint-parser@9.4.3(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0):
dependencies:
debug: 4.4.3(supports-color@7.2.0)
@ -5334,7 +5277,3 @@ snapshots:
yallist@3.1.1: {}
yocto-queue@0.1.0: {}
zrender@5.6.0:
dependencies:
tslib: 2.3.0

View File

@ -76,14 +76,6 @@ export const updateApi = (data) => {
// @Param data body api.CreateApiParams true "更新api"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"更新成功"}"
// @Router /api/setAuthApi [post]
export const setAuthApi = (data) => {
return service({
url: '/api/setAuthApi',
method: 'post',
data
})
}
// @Tags Api
// @Summary 获取所有的Api 不分页
// @Security ApiKeyAuth

View File

@ -102,9 +102,3 @@ export const getSysErrorList = (params) => {
// @Param data query systemReq.SysErrorSearch true "分页获取错误日志列表"
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /sysError/getSysErrorPublic [get]
export const getSysErrorPublic = () => {
return service({
url: '/sysError/getSysErrorPublic',
method: 'get',
})
}

View File

@ -1,47 +0,0 @@
<template>
<VCharts
v-if="renderChart"
:option="options"
:autoresize="autoResize"
:style="{ width, height }"
/>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import VCharts from 'vue-echarts'
import { useWindowResize } from '@/hooks/use-windows-resize'
defineProps({
options: {
type: Object,
default() {
return {}
}
},
autoResize: {
type: Boolean,
default: true
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '100%'
}
})
const renderChart = ref(false)
nextTick(() => {
renderChart.value = true
})
useWindowResize(() => {
renderChart.value = false
nextTick(() => {
renderChart.value = true
})
})
</script>
<style scoped lang="less"></style>

View File

@ -1,5 +1,5 @@
// 基础组件库:以无样式的 reka-ui 原语为底座,仅用项目既有的 UnoCSS 主题 token 上色,
// 跟随 themeStore 换肤 / 暗色模式。规范见 aiDoc/frontend-backend/component-library.md
// 跟随 themeStore 换肤与暗色模式
export * from './button'
export * from './input'
export * from './checkbox'

View File

@ -6,7 +6,6 @@ import * as ElIconModules from '@element-plus/icons-vue'
import svgIcon from '@/components/svgIcon/svgIcon.vue'
// 基础组件库reka-ui 底座src/core/componentLibrary全局以 g- 前缀kebab-case注册
// 全站可直接 <g-button /> / <g-select /> 使用,无需逐文件 import。
// 规范见 aiDoc/frontend-backend/component-library.md
import * as ComponentLibrary from '@/core/componentLibrary'
// 导入转换图标名称的函数

View File

@ -1,187 +0,0 @@
<template>
<Chart :height="height" :option="chartOption" />
</template>
<script setup>
import Chart from '@/components/charts/index.vue'
import useChartOption from '@/hooks/charts'
import { graphic } from 'echarts'
import { computed, ref } from 'vue'
import { useThemeStore } from '@/pinia'
import { storeToRefs } from 'pinia'
import { addOpacityToColor } from '@/theme/color'
const themeStore = useThemeStore()
const { settings } = storeToRefs(themeStore)
defineProps({
height: {
type: String,
default: '128px'
}
})
const axisTextColor = computed(() => {
return themeStore.isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.70)'
})
const dotColor = computed(() => {
return themeStore.isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)'
})
const primaryColor = (opacity) => addOpacityToColor(settings.value.themeColor, opacity)
const graphicFactory = (side) => {
return {
type: 'text',
bottom: '8',
...side,
style: {
text: '',
textAlign: 'center',
fill: axisTextColor.value,
fontSize: 12
}
}
}
const xAxis = ref([
'2024-1',
'2024-2',
'2024-3',
'2024-4',
'2024-5',
'2024-6',
'2024-7',
'2024-8'
])
const chartsData = ref([12, 22, 32, 45, 32, 78, 89, 92])
const graphicElements = ref([
graphicFactory({ left: '5%' }),
graphicFactory({ right: 0 })
])
const { chartOption } = useChartOption(() => {
return {
grid: {
left: '40',
right: '0',
top: '10',
bottom: '30'
},
xAxis: {
type: 'category',
offset: 2,
data: xAxis.value,
boundaryGap: false,
axisLabel: {
color: axisTextColor.value,
formatter(value, idx) {
if (idx === 0) return ''
if (idx === xAxis.value.length - 1) return ''
return `${value}`
}
},
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: true,
interval: (idx) => {
if (idx === 0) return false
if (idx === xAxis.value.length - 1) return false
return true
},
lineStyle: {
color: dotColor.value
}
},
axisPointer: {
show: true,
lineStyle: {
color: primaryColor(1),
width: 2
}
}
},
yAxis: {
type: 'value',
axisLine: {
show: false
},
axisLabel: {
formatter(value, idx) {
if (idx === 0) return value
return `${value}k`
}
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: dotColor.value
}
}
},
tooltip: {
trigger: 'axis',
formatter(params) {
const [firstElement] = params
return `<div>
<p class="tooltip-title">${firstElement.axisValueLabel}</p>
<div class="content-panel"><span>总内容量</span><span class="tooltip-value">${(
Number(firstElement.value) * 10000
).toLocaleString()}</span></div>
</div>`
},
className: 'echarts-tooltip-diy'
},
graphic: {
elements: graphicElements.value
},
series: [
{
data: chartsData.value,
type: 'line',
smooth: true,
// symbol: 'circle',
symbolSize: 12,
emphasis: {
focus: 'series',
itemStyle: {
borderWidth: 2
}
},
lineStyle: {
width: 3,
color: new graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: primaryColor(0.5)
},
{
offset: 0.5,
color: primaryColor(0.57)
},
{
offset: 1,
color: primaryColor(1)
}
])
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: primaryColor(0.13)
},
{
offset: 1,
color: primaryColor(0.03)
}
])
}
}
]
}
})
</script>
<style scoped lang="scss"></style>

View File

@ -1,132 +0,0 @@
<template>
<Chart :height="height" :option="chartOption" />
</template>
<script setup>
import Chart from '@/components/charts/index.vue'
import useChartOption from '@/hooks/charts'
import { graphic } from 'echarts'
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useThemeStore } from '@/pinia'
import { addOpacityToColor } from '@/theme/color'
const themeStore = useThemeStore()
const { settings } = storeToRefs(themeStore)
const prop = defineProps({
height: {
type: String,
default: '128px'
},
data: {
type: Array,
default: () => []
}
})
const graphicFactory = (side) => {
return {
type: 'text',
bottom: '8',
...side,
style: {
text: '',
textAlign: 'center',
fill: themeStore.isDark ? '#FFFFFF' : '#000000',
fontSize: 12
}
}
}
const graphicElements = ref([
graphicFactory({ left: '5%' }),
graphicFactory({ right: 0 })
])
const primaryColor = (opacity) => addOpacityToColor(settings.value.themeColor, opacity)
const { chartOption } = useChartOption(() => {
return {
grid: {
left: '40',
right: '0',
top: '10',
bottom: '30'
},
xAxis: {
type: 'category',
offset: 2,
show: false,
boundaryGap: false,
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
show: false,
axisLine: {
show: false
},
axisLabel: {
show: false
},
splitLine: {
show: false
}
},
graphic: {
elements: graphicElements.value
},
series: [
{
data: prop.data,
type: 'line',
smooth: true,
symbolSize: 12,
emphasis: {
focus: 'series',
itemStyle: {
borderWidth: 2
}
},
lineStyle: {
width: 3,
color: new graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: primaryColor(0.2)
},
{
offset: 0.5,
color: primaryColor(0.39)
},
{
offset: 1,
color: primaryColor(1)
}
])
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: primaryColor(0.13)
},
{
offset: 1,
color: primaryColor(0.03)
}
])
}
}
]
}
})
</script>
<style scoped lang="scss"></style>

View File

@ -1,49 +0,0 @@
<template>
<div class="">
<div class="flex items-center justify-between mb-2">
<div v-if="title" class="text-sm font-semibold tracking-tight text-base-text">
{{ title }}
</div>
<slot v-else name="title" />
</div>
<div class="w-full relative">
<div v-if="type !== 4">
<div class="mt-4 text-3xl font-mono text-base-text">
<el-statistic :value="268500" />
</div>
<div class="mt-2 text-xs font-mono text-muted-foreground">
+80% <el-icon class="align-middle"><TopRight /></el-icon>
</div>
</div>
<div class="absolute top-0 right-2 w-[50%] h-20">
<charts-people-number v-if="type === 1" :data="data[0]" height="100%" />
<charts-people-number v-if="type === 2" :data="data[1]" height="100%" />
<charts-people-number v-if="type === 3" :data="data[2]" height="100%" />
</div>
<charts-content-number v-if="type === 4" height="14rem" />
</div>
</div>
</template>
<script setup>
import chartsPeopleNumber from './charts-people-numbers.vue'
import chartsContentNumber from './charts-content-numbers.vue'
defineProps({
type: {
type: Number,
default: 1
},
title: {
type: String,
default: ''
}
})
const data = [
[12, 22, 32, 45, 32, 78, 89, 92],
[1, 2, 43, 5, 67, 78, 89, 12],
[12, 22, 32, 45, 32, 78, 89, 92]
]
</script>
<style scoped lang="scss"></style>

View File

@ -1,11 +1,9 @@
import KraCard from './card.vue'
import KraChart from './charts.vue'
import KraNotice from './notice.vue'
import KraQuickLink from './quickLinks.vue'
export {
KraCard,
KraChart,
KraNotice,
KraQuickLink,
}

View File

@ -30,37 +30,20 @@
</template>
<script setup>
const notices = [
{
typeTitle: '通知',
time: '今天',
title: 'Kra 管理后台已完成核心系统、公告和邮件模块接入。',
import { onMounted, ref } from 'vue'
import { getInfoList } from '@/modules/announcement/api/info'
const notices = ref([])
onMounted(async () => {
const response = await getInfoList({ page: 1, pageSize: 8, status: 1 })
if (response.code !== 0) return
notices.value = (response.data?.list || []).map((item) => ({
typeTitle: item.type === 2 ? '公告' : '通知',
time: item.createdAt ? new Date(item.createdAt).toLocaleDateString('zh-CN') : '',
title: item.title,
dotClass: 'bg-cyan-500',
tagClass: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/40 dark:text-cyan-200'
},
{
typeTitle: '安全',
time: '2天前',
title: '请在部署前修改 JWT 密钥并配置数据库连接信息。',
dotClass: 'bg-emerald-500',
tagClass: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-200'
},
{
typeTitle: '运维',
time: '3天前',
title: '邮件告警默认关闭,填写 SMTP 配置后自动启用。',
dotClass: 'bg-amber-500',
tagClass: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-200'
},
{
typeTitle: '存储',
time: '5天前',
title: '媒体文件默认存储在本地 uploads/file 目录。',
dotClass: 'bg-violet-500',
tagClass: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-200'
}
]
}))
})
</script>
<style scoped lang="scss"></style>

View File

@ -1,81 +1,38 @@
<template>
<template>
<div class="h-full kra-container2 overflow-auto bg-main">
<div class="space-y-2 py-2">
<kra-card
class="relative overflow-hidden rounded-xl border border-slate-200/80 bg-white px-5 py-6 shadow-sm dark:border-slate-700 dark:bg-slate-900"
>
<div class="relative flex flex-col gap-2 lg:flex-row lg:items-end lg:justify-between">
<div>
<p class="text-xs tracking-[0.2em] text-muted-foreground">DASHBOARD</p>
<h1 class="mt-2 text-xl font-semibold text-base-text lg:text-2xl">
欢迎回来开始今天的Coding节奏
</h1>
<p class="mt-2 text-sm text-muted-foreground">
{{ today }} · 已为你聚合核心业务数据和系统公告
</p>
</div>
</div>
<kra-card class="rounded-xl border border-slate-200/80 bg-white px-5 py-6 shadow-sm dark:border-slate-700 dark:bg-slate-900">
<p class="text-xs tracking-[0.2em] text-muted-foreground">DASHBOARD</p>
<h1 class="mt-2 text-xl font-semibold text-base-text lg:text-2xl">欢迎回来{{ userStore.userInfo.nickName }}</h1>
<p class="mt-2 text-sm text-muted-foreground">{{ today }} · 当前展示后台实时数据</p>
</kra-card>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3">
<kra-card>
<kra-chart :type="1" title="访问人数" />
</kra-card>
<kra-card>
<kra-chart :type="2" title="新增客户" />
</kra-card>
<kra-card>
<kra-chart :type="3" title="解决数量" />
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-4">
<kra-card v-for="item in statistics" :key="item.title">
<div class="p-2"><div class="text-sm text-muted-foreground">{{ item.title }}</div><div class="mt-3 text-3xl font-semibold text-base-text">{{ item.value }}</div></div>
</kra-card>
</div>
<div class="grid grid-cols-1 items-stretch gap-2 xl:grid-cols-12">
<div class="grid grid-cols-1 gap-2 content-start xl:col-span-8 xl:h-full">
<kra-card title="内容数据">
<kra-chart :type="4" />
</kra-card>
</div>
<div class="flex flex-col gap-2 xl:col-span-4 xl:h-full">
<kra-card title="快捷功能" show-action custom-class="min-h-[300px]">
<kra-quick-link />
</kra-card>
<kra-card title="公告" show-action custom-class="min-h-[300px]">
<kra-notice />
</kra-card>
</div>
<div class="grid grid-cols-1 items-stretch gap-2 xl:grid-cols-2">
<kra-card title="快捷功能" show-action custom-class="min-h-[300px]"><kra-quick-link /></kra-card>
<kra-card title="公告" show-action custom-class="min-h-[300px]"><kra-notice /></kra-card>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import {
KraChart,
KraNotice,
KraQuickLink,
KraCard
} from './components'
const today = computed(() => {
try {
const d = new Date()
return d.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
} catch (e) {
return new Date().toISOString().slice(0, 10)
}
})
defineOptions({
name: 'Dashboard'
import { computed, onMounted, ref } from 'vue'
import { useUserStore } from '@/pinia/modules/user'
import { getUserList } from '@/api/user'
import { getSysOperationRecordList } from '@/api/sysOperationRecord'
import { getFileList } from '@/api/fileUploadAndDownload'
import { getInfoList } from '@/modules/announcement/api/info'
import { KraNotice, KraQuickLink, KraCard } from './components'
defineOptions({ name: 'Dashboard' })
const userStore = useUserStore()
const today = computed(() => new Date().toLocaleDateString('zh-CN'))
const statistics = ref([{ title: '系统用户', value: '—' }, { title: '操作记录', value: '—' }, { title: '媒体文件', value: '—' }, { title: '系统公告', value: '—' }])
onMounted(async () => {
const results = await Promise.allSettled([getUserList({ page: 1, pageSize: 1 }), getSysOperationRecordList({ page: 1, pageSize: 1 }), getFileList({ page: 1, pageSize: 1 }), getInfoList({ page: 1, pageSize: 1 })])
results.forEach((result, index) => { if (result.status === 'fulfilled' && result.value?.code === 0) statistics.value[index].value = result.value.data?.total ?? 0 })
})
</script>
<style lang="scss" scoped></style>

View File

@ -1,15 +1,6 @@
<template>
<!-- 工具按钮组扁平图标无独立描边/阴影hover 时图标 morph 为文案取代 tooltip -->
<div class="flex items-center gap-1 mx-3">
<g-dropdown-menu
v-if="isDev"
trigger="hover"
:items="videoList"
@select="(item) => toDoc(item.value)"
>
<icon-button icon="lucide:clapperboard" label="教程" />
</g-dropdown-menu>
<icon-button
v-if="settings.header.search.visible"
icon="lucide:search"
@ -50,8 +41,6 @@
import { emitter } from '@/utils/bus.js'
import CommandMenu from '@/components/commandMenu/index.vue'
import IconButton from '@/components/iconButton/index.vue'
import { toDoc } from '@/utils/doc'
import { isDev } from '@/utils/env.js'
const themeStore = useThemeStore()
const { settings } = storeToRefs(themeStore)
@ -95,62 +84,4 @@
initPage()
const videoList = [
{
label: '1.clone项目和安装依赖',
value: 'https://www.bilibili.com/video/BV1jx4y1s7xx'
},
{
label: '2.初始化项目',
value: 'https://www.bilibili.com/video/BV1sr421K7sv'
},
{
label: '3.开启调试工具+创建初始化包',
value: 'https://www.bilibili.com/video/BV1iH4y1c7Na'
},
{
label: '4.手动使用自动化创建功能',
value: 'https://www.bilibili.com/video/BV1UZ421T7fV'
},
{
label: '5.使用已有表格创建业务',
value: 'https://www.bilibili.com/video/BV1NE4m1977s'
},
{
label: '6.使用AI创建业务和创建数据源模式的可选项',
value: 'https://www.bilibili.com/video/BV17i421a7DE'
},
{
label: '7.创建自己的后端方法',
value: 'https://www.bilibili.com/video/BV1Yw4m1k7fg'
},
{
label: '8.新增一个前端页面',
value: 'https://www.bilibili.com/video/BV12y411i7oE'
},
{
label: '9.配置一个前端二级页面',
value: 'https://www.bilibili.com/video/BV1ZM4m1y7i3'
},
{
label: '10.配置一个前端菜单参数',
value: 'https://www.bilibili.com/video/BV1WS42197DZ'
},
{
label: '11.菜单参数实战+动态菜单标题+菜单高亮配置',
value: 'https://www.bilibili.com/video/BV1NE4m1979c'
},
{
label: '12.增加菜单可控按钮',
value: 'https://www.bilibili.com/video/BV1Sw4m1k746'
},
{
label: '14.新增客户角色和其相关配置教学',
value: 'https://www.bilibili.com/video/BV1Ki421a7X2'
},
{
label: '15.发布项目上线',
value: 'https://www.bilibili.com/video/BV1Lx4y1s77D'
}
]
</script>

View File

@ -32,6 +32,36 @@
</el-form>
</el-tab-pane>
<el-tab-pane label="对象存储" name="storage">
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
<el-form-item label="存储类型" class="md:col-span-2">
<el-select v-model="config.admin.storage.type" class="!w-full">
<el-option v-for="item in storageTypes" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<template v-if="config.admin.storage.type === 'qiniu'">
<el-form-item label="区域"><el-input v-model.trim="config.admin.storage.qiniu.zone" placeholder="ZoneHuadong" /></el-form-item>
<el-form-item label="Bucket"><el-input v-model.trim="config.admin.storage.qiniu.bucket" /></el-form-item>
<el-form-item label="访问域名"><el-input v-model.trim="config.admin.storage.qiniu.base_url" /></el-form-item>
<el-form-item label="Access Key"><el-input v-model.trim="config.admin.storage.qiniu.access_key" /></el-form-item>
<el-form-item label="Secret Key"><el-input v-model="config.admin.storage.qiniu.secret_key" show-password /></el-form-item>
<el-form-item label="连接选项"><div class="flex gap-5"><el-switch v-model="config.admin.storage.qiniu.use_https" active-text="HTTPS" /><el-switch v-model="config.admin.storage.qiniu.use_cdn_domains" active-text="CDN 域名" /></div></el-form-item>
</template>
<template v-else-if="currentObjectStorage">
<el-form-item label="Endpoint"><el-input v-model.trim="currentObjectStorage.endpoint" placeholder="可留空使用提供商默认地址" /></el-form-item>
<el-form-item label="Region"><el-input v-model.trim="currentObjectStorage.region" /></el-form-item>
<el-form-item label="Bucket"><el-input v-model.trim="currentObjectStorage.bucket" /></el-form-item>
<el-form-item label="访问域名"><el-input v-model.trim="currentObjectStorage.base_url" /></el-form-item>
<el-form-item label="Access Key"><el-input v-model.trim="currentObjectStorage.access_key" /></el-form-item>
<el-form-item label="Secret Key"><el-input v-model="currentObjectStorage.secret_key" show-password /></el-form-item>
<el-form-item label="对象前缀"><el-input v-model.trim="currentObjectStorage.path_prefix" /></el-form-item>
<el-form-item v-if="config.admin.storage.type === 'cloudflare-r2'" label="Account ID"><el-input v-model.trim="currentObjectStorage.account_id" /></el-form-item>
<el-form-item label="连接选项"><div class="flex gap-5"><el-switch v-model="currentObjectStorage.use_ssl" active-text="HTTPS" /><el-switch v-model="currentObjectStorage.force_path_style" active-text="Path Style" /></div></el-form-item>
</template>
<p class="md:col-span-2 text-sm text-gray-500">对象存储客户端在服务启动时创建保存后需重启服务生效</p>
</el-form>
</el-tab-pane>
<el-tab-pane label="邮件设置" name="email">
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
<el-form-item label="默认收件人">
@ -68,7 +98,7 @@
</template>
<script setup>
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getSystemConfig, reloadSystem, setSystemConfig } from '@/api/system'
import { emailTest } from '@/api/email'
@ -82,13 +112,22 @@
admin: {
routerPrefix: '',
jwt: { signingKey: '******', issuer: 'kra' },
local: { storePath: 'uploads/file', pathPrefix: 'uploads/file' }
local: { storePath: 'uploads/file', pathPrefix: 'uploads/file' },
storage: { type: 'local', qiniu: {}, aliyun_oss: {}, huawei_obs: {}, tencent_cos: {}, aws_s3: {}, cloudflare_r2: {}, minio: {} }
},
email: {
to: '', from: '', host: '', secret: '******', nickname: '', port: 465,
'is-ssl': true, 'is-loginauth': false
}
})
const storageTypes = [
{ label: '本地存储', value: 'local' }, { label: '七牛云', value: 'qiniu' },
{ label: '阿里云 OSS', value: 'aliyun-oss' }, { label: '华为云 OBS', value: 'huawei-obs' },
{ label: '腾讯云 COS', value: 'tencent-cos' }, { label: 'AWS S3', value: 'aws-s3' },
{ label: 'Cloudflare R2', value: 'cloudflare-r2' }, { label: 'MinIO', value: 'minio' }
]
const storageKeys = { 'aliyun-oss': 'aliyun_oss', 'huawei-obs': 'huawei_obs', 'tencent-cos': 'tencent_cos', 'aws-s3': 'aws_s3', 'cloudflare-r2': 'cloudflare_r2', minio: 'minio' }
const currentObjectStorage = computed(() => config.value.admin.storage[storageKeys[config.value.admin.storage.type]])
const initForm = async () => {
const res = await getSystemConfig()
@ -97,6 +136,15 @@
admin: { ...config.value.admin, ...res.data.config.admin,
jwt: { ...config.value.admin.jwt, ...res.data.config.admin?.jwt },
local: { ...config.value.admin.local, ...res.data.config.admin?.local }
, storage: { ...config.value.admin.storage, ...res.data.config.admin?.storage,
qiniu: { ...config.value.admin.storage.qiniu, ...res.data.config.admin?.storage?.qiniu },
aliyun_oss: { ...config.value.admin.storage.aliyun_oss, ...res.data.config.admin?.storage?.aliyun_oss },
huawei_obs: { ...config.value.admin.storage.huawei_obs, ...res.data.config.admin?.storage?.huawei_obs },
tencent_cos: { ...config.value.admin.storage.tencent_cos, ...res.data.config.admin?.storage?.tencent_cos },
aws_s3: { ...config.value.admin.storage.aws_s3, ...res.data.config.admin?.storage?.aws_s3 },
cloudflare_r2: { ...config.value.admin.storage.cloudflare_r2, ...res.data.config.admin?.storage?.cloudflare_r2 },
minio: { ...config.value.admin.storage.minio, ...res.data.config.admin?.storage?.minio }
}
},
email: { ...config.value.email, ...res.data.config.email }
}

View File

@ -9,7 +9,6 @@ export default defineConfig({
//(如 button 的 buttonVariants不显式纳入扫描的话只出现在这些 .js 里的原子类
//bg-primary / bg-error / hover:bg-primary-600 ...)不会被生成,按钮就会丢失主题色。
// 这里在保留默认文件类型的前提下,额外把 core/componentLibrary 下的 .js/.ts 纳入扫描。
// 详见 aiDoc/frontend-backend/component-library.md。
content: {
pipeline: {
include: [