├── pkg ├── version ├── pack │ ├── .gitignore │ ├── zip_test.go │ ├── tar_test.go │ └── zip.go ├── id.go ├── gist_test.go ├── version.go ├── parse_test.go ├── file_test.go ├── tree.go ├── generate_config.go ├── gist.go ├── enum.go ├── base_rule.go ├── textcolor.go ├── time.go └── eks.go ├── example ├── clone │ └── 1.log ├── media │ └── 2.log └── scanf │ └── 3.log ├── testdata ├── 0 │ └── 39bf8ce4-48f6-4d98-8076-8a74e7353af7 │ │ └── d508482c-9745-4df3-a6e0-a29e8087e21f │ │ └── a │ │ └── 2.log ├── 8 │ └── d508482c-9745-4df3-a6e0-a29e8087e21f │ │ └── a │ │ └── 2.txt ├── efef24cb-fd6e-4aad-b85c-88cf89090028 │ └── a │ │ ├── 2.log │ │ └── aa │ │ └── 1.log └── test.zip ├── compose ├── consul │ ├── .env.example │ ├── docker-compose.yml │ ├── go.mod │ ├── config │ │ └── server.hcl │ ├── main.go │ └── docker-compose.prod.yml ├── kafka │ ├── go.mod │ └── main.go ├── mysql │ └── docker-compose.yml ├── redis │ ├── sentinel1 │ │ └── sentinel.conf │ ├── sentinel2 │ │ └── sentinel.conf │ ├── sentinel3 │ │ └── sentinel.conf │ ├── master │ │ └── redis.conf │ ├── slave1 │ │ └── redis.conf │ ├── slave2 │ │ └── redis.conf │ └── docker-compose.yml └── postgres │ └── docker-compose.yml ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── custom_issue.md │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── mirror.yml │ ├── swagger.yml │ ├── ci.yml │ └── release.yml ├── dependabot.yml ├── prompts │ └── create-api.prompt.md └── PULL_REQUEST_TEMPLATE.md ├── generate.go ├── cmd ├── tools │ └── pr │ │ ├── go.mod │ │ ├── main.go │ │ └── go.sum ├── migrate │ └── migration │ │ ├── custom │ │ └── doc.go │ │ ├── system │ │ ├── 1722316153240_migrate.go │ │ ├── 1724396388009_migrate.go │ │ ├── 1746193492486_migrate.go │ │ └── 1691804837583_tables.go │ │ └── make.go └── cobra.go ├── Makefile ├── config ├── task.go ├── ssl.go ├── auth.go ├── application.yml ├── application.go ├── option_redis.go ├── pyroscope.go └── config.go ├── Dockerfile ├── dto ├── system_config.go ├── api.go ├── user_auth_token.go ├── app_config.go ├── user_config.go ├── languag.go ├── statistics.go ├── option.go ├── tenant.go ├── field.go ├── post.go ├── department.go ├── notice.go ├── model.go ├── task.go ├── role.go ├── monitor.go ├── virtual.go ├── menu.go ├── template.go ├── github.go └── user.go ├── middleware └── init.go ├── main.go ├── models ├── role.go ├── casbin_rule.go ├── system_config.go ├── api.go ├── notice.go ├── option.go ├── language.go ├── task_run.go ├── user_auth_token.go ├── user_config.go ├── user_oauth2.go ├── department.go ├── type.go ├── statistics.go ├── model.go ├── app_config.go ├── field.go └── post.go ├── .gitignore ├── .golangci.yml ├── service ├── statistics.go ├── user_config.go ├── monitor.go └── storage.go ├── LICENSE ├── apis ├── monitor.go ├── ws.go ├── storage.go ├── statistics.go ├── github.go ├── api.go ├── field.go ├── option.go ├── app_config.go ├── system_config.go ├── virtual.go ├── user_config.go └── tenant.go ├── notice └── email │ ├── register_verify_code.html │ ├── password_reset_code.html │ ├── login_verify_code.html │ └── send.go ├── router └── router.go └── center └── type.go /pkg/version: -------------------------------------------------------------------------------- 1 | 0.0.1 -------------------------------------------------------------------------------- /example/clone/1.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/media/2.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/scanf/3.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/efef24cb-fd6e-4aad-b85c-88cf89090028/a/2.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/efef24cb-fd6e-4aad-b85c-88cf89090028/a/aa/1.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/8/d508482c-9745-4df3-a6e0-a29e8087e21f/a/2.txt: -------------------------------------------------------------------------------- 1 | 2112 -------------------------------------------------------------------------------- /pkg/pack/.gitignore: -------------------------------------------------------------------------------- 1 | testdata/*.zip 2 | testdata/*.tar 3 | testdata/*.gz -------------------------------------------------------------------------------- /testdata/0/39bf8ce4-48f6-4d98-8076-8a74e7353af7/d508482c-9745-4df3-a6e0-a29e8087e21f/a/2.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mss-boot-io/mss-boot-admin/HEAD/testdata/test.zip -------------------------------------------------------------------------------- /compose/consul/.env.example: -------------------------------------------------------------------------------- 1 | # Copy this file to .env and fill in the values below 2 | 3 | # Datacenter name 4 | CONSUL_DATACENTER=dc1 5 | 6 | # Gossip encryption key (base64). Generate with: consul keygen 7 | GOSSIP_KEY=REPLACE_WITH_OUTPUT_OF_consul_keygen 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Issues and Suggestions 4 | url: https://github.com/mss-boot-io/mss-boot-admin/issues/new 5 | about: If none of the templates fit, you can submit a custom issue or suggestion. 6 | -------------------------------------------------------------------------------- /compose/kafka/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mss-boot-io/mss-boot-admin/compose/kafka 2 | 3 | go 1.21 4 | 5 | require github.com/segmentio/kafka-go v0.4.47 6 | 7 | require ( 8 | github.com/klauspost/compress v1.15.9 // indirect 9 | github.com/pierrec/lz4/v4 v4.1.15 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /.github/workflows/mirror.yml: -------------------------------------------------------------------------------- 1 | name: 'GitHub Actions Mirror' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | mirror: 10 | uses: mss-boot-io/mss-boot/.github/workflows/mirror-template.yml@main 11 | secrets: 12 | ssh_private_key: ${{ secrets.ssh_private_key }} -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2023/8/6 08:33:26 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2023/8/6 08:33:26 8 | */ 9 | 10 | //go:generate swag init --parseDependency --parseDepth=4 --parseVendor 11 | -------------------------------------------------------------------------------- /cmd/tools/pr/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mss-boot-io/mss-boot-admin/cmd/tools/pr 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/google/go-github v17.0.0+incompatible 7 | github.com/spf13/cast v1.6.0 8 | golang.org/x/oauth2 v0.19.0 9 | ) 10 | 11 | require github.com/google/go-querystring v1.1.0 // indirect 12 | -------------------------------------------------------------------------------- /cmd/migrate/migration/custom/doc.go: -------------------------------------------------------------------------------- 1 | package custom 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2023/8/12 10:11:11 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2023/8/12 10:11:11 8 | */ 9 | 10 | // fixme: developer's custom migration script 11 | func init() { 12 | } 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT:=mss-boot-admin 2 | 3 | .PHONY: build 4 | 5 | build: 6 | CGO_ENABLED=0 go build -o admin main.go 7 | test: 8 | go test -coverprofile=coverage.out ./... 9 | deps: 10 | go mod download 11 | generate: 12 | go generate ./... 13 | 14 | .PHONY: lint 15 | lint: 16 | golangci-lint run -v ./... 17 | fix-lint: 18 | goimports -w . -------------------------------------------------------------------------------- /config/task.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2023/12/5 22:28:26 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2023/12/5 22:28:26 8 | */ 9 | 10 | type Task struct { 11 | Spec string `yaml:"spec" json:"spec"` 12 | Enable bool `yaml:"enable" json:"enable"` 13 | } 14 | -------------------------------------------------------------------------------- /config/ssl.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2024/3/1 10:20:14 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2024/3/1 10:20:14 8 | */ 9 | 10 | type Ssl struct { 11 | KeyStr string 12 | Pem string 13 | Enable bool 14 | Domain string 15 | } 16 | 17 | var SslConfig = new(Ssl) 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | WORKDIR /go/src/github.com/mss-boot-io/mss-boot-admin 4 | 5 | COPY . . 6 | 7 | RUN CGO_ENABLED=0 go build -o admin main.go 8 | 9 | FROM alpine 10 | 11 | LABEL authors="lwnmengjing" 12 | 13 | WORKDIR /app 14 | 15 | COPY --from=builder /go/src/github.com/mss-boot-io/mss-boot-admin/admin /app/admin 16 | 17 | ENTRYPOINT [ "/app/admin" ] -------------------------------------------------------------------------------- /compose/mysql/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | mysql: 4 | image: mysql:latest 5 | restart: always 6 | environment: 7 | MYSQL_DATABASE: mss-boot-admin-local 8 | MYSQL_ROOT_PASSWORD: 123456 9 | ports: 10 | - "3306:3306" 11 | - "33060:33060" 12 | volumes: 13 | - mysql-data:/var/lib/mysql 14 | volumes: 15 | mysql-data: 16 | -------------------------------------------------------------------------------- /dto/system_config.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/mss-boot-io/mss-boot/pkg/response/actions" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2023/12/20 17:50:54 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2023/12/20 17:50:54 10 | */ 11 | 12 | type SystemConfigSearch struct { 13 | actions.Pagination `search:"inline"` 14 | } 15 | -------------------------------------------------------------------------------- /pkg/id.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | /* 10 | * @Author: lwnmengjing 11 | * @Date: 2023/8/12 21:43:02 12 | * @Last Modified by: lwnmengjing 13 | * @Last Modified time: 2023/8/12 21:43:02 14 | */ 15 | 16 | // SimpleID simple id 17 | func SimpleID() string { 18 | return strings.ReplaceAll(uuid.New().String(), "-", "") 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom Issue 3 | about: Create a custom issue or suggestion 4 | title: '' 5 | labels: '' 6 | assignees: 7 | - 'lwnmengjing' 8 | projects: 9 | - 'mss-boot-admin' 10 | --- 11 | 12 | **Issue or Suggestion** 13 | A clear and concise description of your issue or suggestion. 14 | 15 | **Relevant Information** 16 | Add any other context, information, or screenshots about the issue here. 17 | -------------------------------------------------------------------------------- /compose/redis/sentinel1/sentinel.conf: -------------------------------------------------------------------------------- 1 | # 所有哨兵端口都一致,因为使用 Docker 桥接网络映射 2 | port 26379 3 | 4 | # 哨兵设置,所有哨兵皆一致,都指向 Master 5 | sentinel monitor mymaster 172.25.0.101 6379 2 6 | sentinel parallel-syncs mymaster 1 7 | sentinel down-after-milliseconds mymaster 30000 8 | sentinel failover-timeout mymaster 180000 9 | 10 | bind 0.0.0.0 11 | protected-mode no 12 | daemonize no 13 | pidfile /var/run/redis-sentinel.pid 14 | logfile "" 15 | dir /tmp 16 | -------------------------------------------------------------------------------- /compose/redis/sentinel2/sentinel.conf: -------------------------------------------------------------------------------- 1 | # 所有哨兵端口都一致,因为使用 Docker 桥接网络映射 2 | port 26379 3 | 4 | # 哨兵设置,所有哨兵皆一致,都指向 Master 5 | sentinel monitor mymaster 172.25.0.101 6379 2 6 | sentinel parallel-syncs mymaster 1 7 | sentinel down-after-milliseconds mymaster 30000 8 | sentinel failover-timeout mymaster 180000 9 | 10 | bind 0.0.0.0 11 | protected-mode no 12 | daemonize no 13 | pidfile /var/run/redis-sentinel.pid 14 | logfile "" 15 | dir /tmp 16 | -------------------------------------------------------------------------------- /compose/redis/sentinel3/sentinel.conf: -------------------------------------------------------------------------------- 1 | # 所有哨兵端口都一致,因为使用 Docker 桥接网络映射 2 | port 26379 3 | 4 | # 哨兵设置,所有哨兵皆一致,都指向 Master 5 | sentinel monitor mymaster 172.25.0.101 6379 2 6 | sentinel parallel-syncs mymaster 1 7 | sentinel down-after-milliseconds mymaster 30000 8 | sentinel failover-timeout mymaster 180000 9 | 10 | bind 0.0.0.0 11 | protected-mode no 12 | daemonize no 13 | pidfile /var/run/redis-sentinel.pid 14 | logfile "" 15 | dir /tmp 16 | -------------------------------------------------------------------------------- /middleware/init.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | var Middlewares = &sync.Map{} 10 | 11 | // GetMiddlewares get middlewares by keys 12 | func GetMiddlewares(keys ...string) gin.HandlersChain { 13 | var mws gin.HandlersChain 14 | for _, key := range keys { 15 | if v, ok := Middlewares.Load(key); ok { 16 | mws = append(mws, v.(gin.HandlerFunc)) 17 | } 18 | } 19 | return mws 20 | } 21 | -------------------------------------------------------------------------------- /dto/api.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 5 | ) 6 | 7 | /* 8 | * @Author: lwnmengjing 9 | * @Date: 2023/8/24 01:48:31 10 | * @Last Modified by: lwnmengjing 11 | * @Last Modified time: 2023/8/24 01:48:31 12 | */ 13 | 14 | type APISearch struct { 15 | actions.Pagination `search:"inline"` 16 | // ID 17 | ID string `query:"id" form:"id" search:"type:contains;column:id"` 18 | } 19 | -------------------------------------------------------------------------------- /dto/user_auth_token.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "time" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2024/7/30 14:10:02 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2024/7/30 14:10:02 10 | */ 11 | 12 | type ResponseNonce struct { 13 | Nonce string `json:"nonce"` 14 | } 15 | 16 | type UserAuthTokenGenerateRequest struct { 17 | ValidityPeriod time.Duration `form:"validityPeriod" query:"validityPeriod"` 18 | } 19 | -------------------------------------------------------------------------------- /dto/app_config.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2024/1/11 17:36:42 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2024/1/11 17:36:42 8 | */ 9 | 10 | type AppConfigGroupRequest struct { 11 | Group string `uri:"group" binding:"required"` 12 | } 13 | 14 | type AppConfigControlRequest struct { 15 | Group string `uri:"group" binding:"required" swaggerignore:"true"` 16 | Data map[string]any `json:"data" binding:"required"` 17 | } 18 | -------------------------------------------------------------------------------- /dto/user_config.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2024/3/2 00:53:53 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2024/3/2 00:53:53 8 | */ 9 | 10 | type UserConfigGroupRequest struct { 11 | Group string `uri:"group" binding:"required"` 12 | } 13 | 14 | type UserConfigControlRequest struct { 15 | Group string `uri:"group" binding:"required" swaggerignore:"true"` 16 | Data map[string]any `json:"data" binding:"required"` 17 | } 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/mss-boot-io/mss-boot-admin/cmd" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2023/8/6 08:33:26 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2023/8/6 08:33:26 10 | */ 11 | 12 | // @title admin API 13 | // @version 0.0.1 14 | // @description admin接口文档 15 | // @securityDefinitions.apikey Bearer 16 | // @in header 17 | // @name Authorization 18 | // @host localhost:8080 19 | // @BasePath 20 | func main() { 21 | cmd.Execute() 22 | } 23 | -------------------------------------------------------------------------------- /pkg/gist_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_GistClone(t *testing.T) { 8 | tests := []struct { 9 | id string 10 | dir string 11 | token string 12 | want []string 13 | }{ 14 | { 15 | id: "1", 16 | dir: "../test", 17 | want: []string{}, 18 | }, 19 | } 20 | for _, tt := range tests { 21 | t.Run(tt.id, func(t *testing.T) { 22 | err := GistClone(tt.id, tt.dir, tt.token) 23 | if err != nil { 24 | t.Errorf("GistClone() err %v", err) 25 | } 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /dto/languag.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/mss-boot-io/mss-boot/pkg/response/actions" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2023/12/12 12:08:11 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2023/12/12 12:08:11 10 | */ 11 | 12 | type LanguageSearch struct { 13 | actions.Pagination `search:"inline"` 14 | // ID 15 | ID string `query:"id" form:"id" search:"type:contains;column:id"` 16 | //名称 17 | Name string `query:"name" form:"name" search:"type:contains;column:name"` 18 | } 19 | -------------------------------------------------------------------------------- /compose/postgres/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | pgsql: 4 | image: postgres:latest 5 | container_name: pgsql 6 | restart: always 7 | environment: 8 | POSTGRES_USER: postgres # PostgreSQL的默认超级用户名 9 | POSTGRES_PASSWORD: 123456 # 设置超级用户的密码 10 | POSTGRES_DB: mss-boot-admin-local # 创建的数据库名 11 | TZ: Asia/Shanghai # 设置时区 12 | ports: 13 | - "5432:5432" # 将容器的5432端口映射到主机的5432端口 14 | volumes: 15 | - postgres-data:/var/lib/postgresql/data # 挂载数据卷,替换/path/to/your/data为你希望存储数据的路径 16 | volumes: 17 | postgres-data: 18 | -------------------------------------------------------------------------------- /config/auth.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "time" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2023/8/12 23:22:37 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2023/8/12 23:22:37 10 | */ 11 | 12 | type Auth struct { 13 | Realm string `yaml:"realm" json:"realm"` 14 | Key string `yaml:"key" json:"key"` 15 | IdentityKey string `yaml:"identityKey" json:"identityKey"` 16 | Timeout time.Duration `yaml:"timeout" json:"timeout"` 17 | MaxRefresh time.Duration `yaml:"maxRefresh" json:"maxRefresh"` 18 | } 19 | -------------------------------------------------------------------------------- /dto/statistics.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2024/1/12 17:53:32 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2024/1/12 17:53:32 8 | */ 9 | 10 | type StatisticsGetRequest struct { 11 | Name string `uri:"name" binding:"required"` 12 | } 13 | 14 | type StatisticsGetResponse struct { 15 | Name string `json:"name"` 16 | Type string `json:"type"` 17 | Items []StatisticsItem `json:"items"` 18 | } 19 | 20 | type StatisticsItem struct { 21 | Date string `json:"time"` 22 | Scales float64 `json:"scales"` 23 | } 24 | -------------------------------------------------------------------------------- /pkg/version.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import "embed" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2023/8/10 00:26:51 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2023/8/10 00:26:51 10 | */ 11 | 12 | //go:embed version 13 | var versionFS embed.FS 14 | 15 | // Version is the version of the binary 16 | var Version string 17 | 18 | // fixme 这里后面可能会改为读取CHNAGELOG.md文件中的版本号 19 | func init() { 20 | if Version == "" { 21 | rb, err := versionFS.ReadFile("version") 22 | if err != nil { 23 | Version = "unknown" 24 | } else { 25 | Version = string(rb) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /dto/option.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/mss-boot-io/mss-boot/pkg/response/actions" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2024/1/1 12:06:44 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2024/1/1 12:06:44 10 | */ 11 | 12 | type OptionSearch struct { 13 | actions.Pagination `search:"inline"` 14 | // ID 15 | ID string `query:"id" form:"id" search:"type:contains;column:id"` 16 | // Name 名称 17 | Name string `query:"name" form:"name" search:"type:contains;column:name"` 18 | // Status 状态 19 | Status string `query:"status" form:"status" search:"type:exact;column:status"` 20 | } 21 | -------------------------------------------------------------------------------- /dto/tenant.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/mss-boot-io/mss-boot/pkg/response/actions" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2024/1/8 18:13:17 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2024/1/8 18:13:17 10 | */ 11 | 12 | type TenantSearch struct { 13 | actions.Pagination `search:"inline"` 14 | // ID 15 | ID string `query:"id" form:"id" search:"type:contains;column:id"` 16 | // Name 名称 17 | Name string `query:"name" form:"name" search:"type:contains;column:name"` 18 | // Status 状态 19 | Status string `query:"status" form:"status" search:"type:contains;column:status"` 20 | } 21 | -------------------------------------------------------------------------------- /dto/field.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/mss-boot-io/mss-boot/pkg/response/actions" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2023/12/29 21:56:21 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2023/12/29 21:56:21 10 | */ 11 | 12 | type FieldSearch struct { 13 | actions.Pagination `search:"inline"` 14 | // ID 15 | ID string `query:"id" form:"id" search:"type:contains;column:id"` 16 | // Name 名称 17 | Name string `query:"name" form:"name" search:"type:contains;column:name"` 18 | // ModelID 模型id 19 | ModelID string `query:"modelID" form:"modelID" search:"type:exact;column:model_id"` 20 | } 21 | -------------------------------------------------------------------------------- /dto/post.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/mss-boot-io/mss-boot/pkg/response/actions" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2024/1/29 00:22:47 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2024/1/29 00:22:47 10 | */ 11 | 12 | type PostSearch struct { 13 | actions.Pagination `search:"inline"` 14 | // ID 15 | ID string `query:"id" form:"id" search:"type:contains;column:id"` 16 | // Name 名称 17 | Name string `query:"name" form:"name" search:"type:contains;column:name"` 18 | // ParentID 父级部门ID 19 | ParentID string `query:"parentID" form:"parentID" search:"type:exact;column:parent_id"` 20 | } 21 | -------------------------------------------------------------------------------- /dto/department.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/mss-boot-io/mss-boot/pkg/response/actions" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2024/1/29 00:17:28 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2024/1/29 00:17:28 10 | */ 11 | 12 | type DepartmentSearch struct { 13 | actions.Pagination `search:"inline"` 14 | // ID 15 | ID string `query:"id" form:"id" search:"type:contains;column:id"` 16 | // Name 名称 17 | Name string `query:"name" form:"name" search:"type:contains;column:name"` 18 | // ParentID 父级部门ID 19 | ParentID string `query:"parentID" form:"parentID" search:"type:exact;column:parent_id"` 20 | } 21 | -------------------------------------------------------------------------------- /models/role.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2023/8/6 08:33:26 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2023/8/6 08:33:26 8 | */ 9 | 10 | import ( 11 | "github.com/mss-boot-io/mss-boot/pkg/enum" 12 | ) 13 | 14 | type Role struct { 15 | ModelGormTenant 16 | Name string `json:"name"` 17 | Root bool `json:"root" gorm:"->"` 18 | Default bool `json:"default" gorm:"->"` 19 | Status enum.Status `json:"status" gorm:"size:10"` 20 | Remark string `json:"remark" gorm:"type:text"` 21 | } 22 | 23 | func (*Role) TableName() string { 24 | return "mss_boot_roles" 25 | } 26 | -------------------------------------------------------------------------------- /compose/consul/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | consul-server: 4 | image: hashicorp/consul:latest 5 | command: "agent -server -bootstrap-expect=1 -ui -node=consul-server -bind=0.0.0.0 -client=0.0.0.0" 6 | ports: 7 | - "8500:8500" 8 | - "8300:8300" 9 | - "8301:8301" 10 | - "8302:8302" 11 | - "8400:8400" 12 | - "8600:8600" 13 | volumes: 14 | - consul-data:/consul/data 15 | consul-client: 16 | image: hashicorp/consul:latest 17 | command: "agent -join=consul-server -node=consul-client -bind=0.0.0.0 -client=0.0.0.0" 18 | links: 19 | - consul-server 20 | depends_on: 21 | - consul-server 22 | volumes: 23 | consul-data: -------------------------------------------------------------------------------- /cmd/migrate/migration/system/1722316153240_migrate.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/mss-boot-io/mss-boot-admin/models" 5 | "runtime" 6 | 7 | "github.com/mss-boot-io/mss-boot/pkg/migration" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | func init() { 12 | _, fileName, _, _ := runtime.Caller(0) 13 | migration.Migrate.SetVersion(migration.GetFilename(fileName), _1722316153240Migrate) 14 | } 15 | 16 | func _1722316153240Migrate(db *gorm.DB, version string) error { 17 | return db.Transaction(func(tx *gorm.DB) error { 18 | err := tx.Migrator().AutoMigrate(new(models.UserAuthToken)) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | return migration.Migrate.CreateVersion(tx, version) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/parse_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "text/template/parse" 7 | ) 8 | 9 | func TestGetKeys(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | args string 13 | want []string 14 | }{ 15 | { 16 | name: "test0", 17 | args: "{{.ab}}dsdf{{.b}}{{.c}} {{$abe = .a}}", 18 | want: []string{"ab", "b", "c", "a"}, 19 | }, 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | d, _ := parse.Parse(tt.name, tt.args, "{{", "}}") 24 | got := getParseKeys(d[tt.name].Root) 25 | if !reflect.DeepEqual(got, tt.want) { 26 | t.Errorf("GetParseKeys() = %v, want %v", got, tt.want) 27 | } 28 | t.Log(got) 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cmd/migrate/migration/system/1724396388009_migrate.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/mss-boot-io/mss-boot/pkg/migration" 7 | "gorm.io/gorm" 8 | 9 | "github.com/mss-boot-io/mss-boot-admin/models" 10 | ) 11 | 12 | func init() { 13 | _, fileName, _, _ := runtime.Caller(0) 14 | migration.Migrate.SetVersion(migration.GetFilename(fileName), _1724396388009Migrate) 15 | } 16 | 17 | func _1724396388009Migrate(db *gorm.DB, version string) error { 18 | return db.Transaction(func(tx *gorm.DB) error { 19 | 20 | err := tx.Migrator().AutoMigrate( 21 | new(models.Task), 22 | ) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | return migration.Migrate.CreateVersion(tx, version) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /models/casbin_rule.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2023/8/25 17:24:19 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2023/8/25 17:24:19 8 | */ 9 | 10 | type CasbinRule struct { 11 | ID int `json:"id" gorm:"column:id"` 12 | PType string `json:"ptype" gorm:"column:ptype"` 13 | V0 string `json:"v0" gorm:"column:v0"` 14 | V1 string `json:"v1" gorm:"column:v1"` 15 | V2 string `json:"v2" gorm:"column:v2"` 16 | V3 string `json:"v3" gorm:"column:v3"` 17 | V4 string `json:"v4" gorm:"column:v4"` 18 | V5 string `json:"v5" gorm:"column:v5"` 19 | } 20 | 21 | func (*CasbinRule) TableName() string { 22 | return "mss_boot_casbin_rule" 23 | } 24 | -------------------------------------------------------------------------------- /pkg/file_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestGetSubPath(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | args string 12 | want []string 13 | wantErr bool 14 | }{ 15 | { 16 | "test0", 17 | "../example", 18 | []string{"clone", "media", "scanf"}, 19 | false, 20 | }, 21 | } 22 | for _, tt := range tests { 23 | t.Run(tt.name, func(t *testing.T) { 24 | got, err := GetSubPath(tt.args) 25 | if (err != nil) != tt.wantErr { 26 | t.Errorf("GetSubPath() error = %v, wantErr %v", err, tt.wantErr) 27 | return 28 | } 29 | if !reflect.DeepEqual(got, tt.want) { 30 | t.Errorf("GetSubPath() got = %v, want %v", got, tt.want) 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | coverage* 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Go workspace file 22 | go.work 23 | go.work* 24 | 25 | # IDE 26 | .vscode 27 | .idea 28 | 29 | config/application-* 30 | *.db 31 | 32 | temp 33 | test 34 | public 35 | docs 36 | dist/* 37 | mss-boot-admin* 38 | compose/**/**/data -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] " 5 | labels: enhancement 6 | assignees: 7 | - 'lwnmengjing' 8 | projects: 9 | - 'mss-boot-admin' 10 | --- 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /pkg/tree.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2024/1/28 11:09:48 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2024/1/28 11:09:48 8 | */ 9 | 10 | type TreeImp interface { 11 | GetIndex() string 12 | GetParentID() string 13 | AddChildren([]TreeImp) 14 | SortChildren() 15 | } 16 | 17 | // BuildTree 使用递归实现树 18 | func BuildTree(list []TreeImp, parentID string) []TreeImp { 19 | if len(list) == 0 { 20 | return nil 21 | } 22 | var tree []TreeImp 23 | for i := range list { 24 | if list[i].GetParentID() == parentID { 25 | list[i].AddChildren(BuildTree(list, list[i].GetIndex())) 26 | list[i].SortChildren() 27 | tree = append(tree, list[i]) 28 | } 29 | } 30 | return tree 31 | } 32 | -------------------------------------------------------------------------------- /dto/notice.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/mss-boot-io/mss-boot/pkg/response/actions" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2023/12/19 00:14:23 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2023/12/19 00:14:23 10 | */ 11 | 12 | type NoticeSearch struct { 13 | actions.Pagination `search:"inline"` 14 | // UserID 用户ID 15 | UserID string `query:"userID" form:"userID" search:"type:contains;column:user_id"` 16 | // Title 标题 17 | Title string `query:"title" form:"title" search:"type:contains;column:title"` 18 | // Status 状态 19 | Status string `query:"status" form:"status" search:"type:exact;column:status"` 20 | // Type 类型 21 | Type string `query:"type" form:"type" search:"type:exact;column:type"` 22 | } 23 | -------------------------------------------------------------------------------- /dto/model.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 5 | ) 6 | 7 | /* 8 | * @Author: lwnmengjing 9 | * @Date: 2023/9/18 12:56:46 10 | * @Last Modified by: lwnmengjing 11 | * @Last Modified time: 2023/9/18 12:56:46 12 | */ 13 | 14 | type ModelSearch struct { 15 | actions.Pagination `search:"inline"` 16 | // ID 17 | ID string `query:"id" form:"id" search:"type:contains;column:id"` 18 | //名称 19 | Name string `query:"name" form:"name" search:"type:contains;column:name"` 20 | } 21 | 22 | type ModelGenerateDataRequest struct { 23 | ID string `json:"id" binding:"required"` 24 | MenuParentID string `json:"menuParentID"` 25 | } 26 | 27 | type ModelCreateMenuRequest struct { 28 | ID string `json:"id" binding:"required"` 29 | ParentID string `json:"parentID"` 30 | } 31 | -------------------------------------------------------------------------------- /compose/redis/master/redis.conf: -------------------------------------------------------------------------------- 1 | port 6379 2 | pidfile /var/run/redis_6379.pid 3 | protected-mode no 4 | timeout 0 5 | tcp-keepalive 300 6 | loglevel notice 7 | 8 | ################################# REPLICATION ################################# 9 | slave-serve-stale-data yes 10 | slave-read-only yes 11 | repl-diskless-sync no 12 | repl-diskless-sync-delay 5 13 | repl-disable-tcp-nodelay no 14 | 15 | ##################################### RDB ##################################### 16 | dbfilename dump.rdb 17 | save 900 1 18 | save 300 10 19 | save 60 10000 20 | stop-writes-on-bgsave-error yes 21 | rdbcompression yes 22 | rdbchecksum yes 23 | dir ./ 24 | 25 | ##################################### AOF ##################################### 26 | appendonly yes 27 | appendfilename "appendonly.aof" 28 | appendfsync everysec 29 | no-appendfsync-on-rewrite no 30 | aof-load-truncated yes 31 | aof-use-rdb-preamble no 32 | -------------------------------------------------------------------------------- /pkg/generate_config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lwnmengjing 3 | * @Date: 2021/12/16 7:39 下午 4 | * @Last Modified by: lwnmengjing 5 | * @Last Modified time: 2021/12/16 7:39 下午 6 | */ 7 | 8 | package pkg 9 | 10 | import ( 11 | "fmt" 12 | ) 13 | 14 | type TemplateConfig struct { 15 | Service string `yaml:"service"` 16 | TemplateUrl string `yaml:"templateUrl"` 17 | TemplateLocal string `yaml:"templateLocal"` 18 | TemplateLocalSubPath string `yaml:"templateLocalSubPath"` 19 | CreateRepo bool `yaml:"createRepo"` 20 | Destination string `yaml:"destination"` 21 | Github *GithubConfig `yaml:"github"` 22 | Params interface{} `yaml:"params"` 23 | Ignore []string `yaml:"ignore"` 24 | } 25 | 26 | func (e *TemplateConfig) OnChange() { 27 | fmt.Println("config changed") 28 | } 29 | -------------------------------------------------------------------------------- /dto/task.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/mss-boot-io/mss-boot/pkg/response/actions" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2023/12/7 13:26:39 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2023/12/7 13:26:39 10 | */ 11 | 12 | type TaskSearch struct { 13 | actions.Pagination `search:"inline"` 14 | // ID 15 | ID string `query:"id" form:"id" search:"type:contains;column:id"` 16 | //名称 17 | Name string `query:"name" form:"name" search:"type:contains;column:name"` 18 | //状态 19 | Status string `query:"status" form:"status" search:"type:exact;column:status"` 20 | } 21 | 22 | type TaskOperateRequest struct { 23 | ID string `uri:"id" binding:"required"` 24 | Operate string `uri:"operate" binding:"required"` 25 | } 26 | 27 | type TaskFuncItem struct { 28 | Name string `json:"name"` 29 | Description string `json:"description"` 30 | } 31 | -------------------------------------------------------------------------------- /cmd/migrate/migration/system/1746193492486_migrate.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/mss-boot-io/mss-boot/pkg/migration" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | func init() { 11 | _, fileName, _, _ := runtime.Caller(0) 12 | migration.Migrate.SetVersion(migration.GetFilename(fileName), _1746193492486Migrate) 13 | } 14 | 15 | func _1746193492486Migrate(db *gorm.DB, version string) error { 16 | return db.Transaction(func(tx *gorm.DB) error { 17 | 18 | err := tx. 19 | Exec(`CREATE UNIQUE INDEX idx_user_config ON mss_boot_user_configs(tenant_id, user_id, name, "group")`). 20 | Error 21 | if err != nil { 22 | return err 23 | } 24 | 25 | err = tx. 26 | Exec(`CREATE UNIQUE INDEX idx_app_config ON mss_boot_app_configs(tenant_id, name, "group")`). 27 | Error 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return migration.Migrate.CreateVersion(tx, version) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /compose/redis/slave1/redis.conf: -------------------------------------------------------------------------------- 1 | port 6379 2 | pidfile /var/run/redis_6379.pid 3 | protected-mode no 4 | timeout 0 5 | tcp-keepalive 300 6 | loglevel notice 7 | 8 | ################################# REPLICATION ################################# 9 | slave-serve-stale-data yes 10 | slave-read-only yes 11 | repl-diskless-sync no 12 | repl-diskless-sync-delay 5 13 | repl-disable-tcp-nodelay no 14 | #slaveof 172.25.0.101 6379 15 | replicaof 172.25.0.101 6379 16 | 17 | ##################################### RDB ##################################### 18 | dbfilename dump.rdb 19 | save 900 1 20 | save 300 10 21 | save 60 10000 22 | stop-writes-on-bgsave-error yes 23 | rdbcompression yes 24 | rdbchecksum yes 25 | dir ./ 26 | 27 | ##################################### AOF ##################################### 28 | appendonly yes 29 | appendfilename "appendonly.aof" 30 | appendfsync everysec 31 | no-appendfsync-on-rewrite no 32 | aof-load-truncated yes 33 | aof-use-rdb-preamble no 34 | -------------------------------------------------------------------------------- /compose/redis/slave2/redis.conf: -------------------------------------------------------------------------------- 1 | port 6379 2 | pidfile /var/run/redis_6379.pid 3 | protected-mode no 4 | timeout 0 5 | tcp-keepalive 300 6 | loglevel notice 7 | 8 | ################################# REPLICATION ################################# 9 | slave-serve-stale-data yes 10 | slave-read-only yes 11 | repl-diskless-sync no 12 | repl-diskless-sync-delay 5 13 | repl-disable-tcp-nodelay no 14 | #slaveof 172.25.0.101 6379 15 | replicaof 172.25.0.101 6379 16 | 17 | ##################################### RDB ##################################### 18 | dbfilename dump.rdb 19 | save 900 1 20 | save 300 10 21 | save 60 10000 22 | stop-writes-on-bgsave-error yes 23 | rdbcompression yes 24 | rdbchecksum yes 25 | dir ./ 26 | 27 | ##################################### AOF ##################################### 28 | appendonly yes 29 | appendfilename "appendonly.aof" 30 | appendfsync everysec 31 | no-appendfsync-on-rewrite no 32 | aof-load-truncated yes 33 | aof-use-rdb-preamble no 34 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | path: ./... 3 | deadline: 5m 4 | skip-dirs: 5 | - vendor 6 | - proto 7 | - test 8 | skip-files: 9 | - ".*\\_test\\.go$" 10 | - ".*\\_string\\.go$" 11 | linters-settings: 12 | lll: 13 | line-length: 170 14 | dupl: 15 | threshold: 400 16 | 17 | issues: 18 | # don't skip warning about doc comments 19 | exclude-use-default: false 20 | 21 | # restore some of the defaults 22 | # (fill in the rest as needed) 23 | exclude-rules: 24 | - linters: [errcheck] 25 | text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*printf?|os\\.(Un)?Setenv). is not checked" 26 | linters: 27 | disable-all: true 28 | enable: 29 | - misspell 30 | # - structcheck 31 | # - golint 32 | - govet 33 | # - deadcode 34 | - errcheck 35 | # - varcheck 36 | - goconst 37 | - unparam 38 | - ineffassign 39 | - nakedret 40 | - gocyclo 41 | - lll 42 | - dupl 43 | - goimports -------------------------------------------------------------------------------- /compose/consul/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mss-boot-io/mss-boot-admin/compose/consul 2 | 3 | go 1.22 4 | 5 | require github.com/hashicorp/consul/api v1.28.2 6 | 7 | require ( 8 | github.com/armon/go-metrics v0.4.1 // indirect 9 | github.com/fatih/color v1.14.1 // indirect 10 | github.com/hashicorp/errwrap v1.1.0 // indirect 11 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 12 | github.com/hashicorp/go-hclog v1.5.0 // indirect 13 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 14 | github.com/hashicorp/go-multierror v1.1.1 // indirect 15 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 16 | github.com/hashicorp/golang-lru v0.5.4 // indirect 17 | github.com/hashicorp/serf v0.10.1 // indirect 18 | github.com/mattn/go-colorable v0.1.13 // indirect 19 | github.com/mattn/go-isatty v0.0.17 // indirect 20 | github.com/mitchellh/go-homedir v1.1.0 // indirect 21 | github.com/mitchellh/mapstructure v1.5.0 // indirect 22 | golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect 23 | golang.org/x/sys v0.15.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /service/statistics.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/mss-boot-io/mss-boot-admin/center" 6 | "github.com/mss-boot-io/mss-boot-admin/dto" 7 | "github.com/mss-boot-io/mss-boot-admin/models" 8 | ) 9 | 10 | /* 11 | * @Author: lwnmengjing 12 | * @Date: 2024/1/12 17:50:50 13 | * @Last Modified by: lwnmengjing 14 | * @Last Modified time: 2024/1/12 17:50:50 15 | */ 16 | 17 | type Statistics struct{} 18 | 19 | func (*Statistics) Get(ctx *gin.Context, name string) (*dto.StatisticsGetResponse, error) { 20 | list := make([]*models.Statistics, 0) 21 | err := center.GetDB(ctx, &models.Statistics{}).Where("name = ?", name).Find(&list).Error 22 | if err != nil { 23 | return nil, err 24 | } 25 | result := &dto.StatisticsGetResponse{ 26 | Name: name, 27 | Items: make([]dto.StatisticsItem, len(list)), 28 | } 29 | for i := range list { 30 | result.Items[i].Date = list[i].Time 31 | result.Items[i].Scales = float64(list[i].Value) / 100 32 | } 33 | return result, nil 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: 7 | - 'lwnmengjing' 8 | projects: 9 | - 'mss-boot-admin' 10 | --- 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Desktop (please complete the following information):** 29 | - OS: [e.g., Windows 10] 30 | - Browser: [e.g., Chrome, Safari] 31 | - Version: [e.g., 22] 32 | 33 | **Smartphone (please complete the following information):** 34 | - Device: [e.g., iPhone X] 35 | - OS: [e.g., iOS 14.4] 36 | - Browser: [e.g., stock browser, Safari] 37 | - Version: [e.g., 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /dto/role.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2023/8/6 08:33:26 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2023/8/6 08:33:26 8 | */ 9 | 10 | import ( 11 | "github.com/mss-boot-io/mss-boot/pkg/enum" 12 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 13 | ) 14 | 15 | type RoleSearch struct { 16 | actions.Pagination `search:"inline"` 17 | // ID 18 | ID string `query:"id" form:"id" search:"type:contains;column:id"` 19 | //名称 20 | Name string `query:"name" form:"name" search:"type:contains;column:name"` 21 | //状态 22 | Status enum.Status `query:"status" form:"status" search:"type:exact;column:status"` 23 | //备注 24 | Remark string `query:"remark" form:"remark" search:"type:contains;column:remark"` 25 | } 26 | 27 | type SetAuthorizeRequest struct { 28 | RoleID string `uri:"roleID" swaggerignore:"true" binding:"required"` 29 | Paths []string `json:"paths"` 30 | } 31 | 32 | type GetAuthorizeResponse struct { 33 | RoleID string `json:"roleID"` 34 | Paths []string `json:"paths,omitempty"` 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 mss-boot-io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/gist.go: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: snakelu 3 | * @Date: 2023/02/24 9:55 上午 4 | * @Last Modified by: snakelu 5 | * @Last Modified time: 2023/02/24 9:55 上午 6 | */ 7 | 8 | package pkg 9 | 10 | import ( 11 | "bytes" 12 | "context" 13 | "log" 14 | "net/http" 15 | "os" 16 | "path/filepath" 17 | 18 | "github.com/google/go-github/v41/github" 19 | "golang.org/x/oauth2" 20 | ) 21 | 22 | // GistClone clone gist repo 23 | func GistClone(id, dir, accessToken string) error { 24 | ctx := context.Background() 25 | var tc *http.Client 26 | if accessToken != "" { 27 | ts := oauth2.StaticTokenSource( 28 | &oauth2.Token{AccessToken: accessToken}, 29 | ) 30 | tc = oauth2.NewClient(ctx, ts) 31 | } 32 | 33 | client := github.NewClient(tc) 34 | gist, _, err := client.Gists.Get(ctx, id) 35 | if err != nil { 36 | log.Fatal(err) 37 | return err 38 | } 39 | 40 | if !PathExist(dir) { 41 | _ = PathCreate(dir) 42 | } 43 | 44 | // copy file to directory 45 | for _, f := range gist.Files { 46 | err = FileOpen(*bytes.NewBufferString(f.GetContent()), filepath.Join(dir, f.GetFilename()), os.ModePerm) 47 | if err != nil { 48 | return err 49 | } 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/enum.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2023/12/21 10:35:52 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2023/12/21 10:35:52 8 | */ 9 | 10 | type AccessType string 11 | 12 | const ( 13 | // DirectoryAccessType 目录类型 14 | DirectoryAccessType AccessType = "DIRECTORY" 15 | // MenuAccessType 菜单类型 16 | MenuAccessType AccessType = "MENU" 17 | // APIAccessType API类型 18 | APIAccessType AccessType = "API" 19 | // ComponentAccessType 组件类型 20 | ComponentAccessType AccessType = "COMPONENT" 21 | ) 22 | 23 | func (a AccessType) String() string { 24 | return string(a) 25 | } 26 | 27 | type LoginProvider string 28 | 29 | const ( 30 | // GithubLoginProvider github oauth provider 31 | GithubLoginProvider LoginProvider = "github" 32 | // LarkLoginProvider lark oauth provider 33 | LarkLoginProvider LoginProvider = "lark" 34 | // EmailLoginProvider email login provider 35 | EmailLoginProvider LoginProvider = "email" 36 | // EmailRegisterProvider email register provider 37 | EmailRegisterProvider LoginProvider = "email_register" 38 | ) 39 | 40 | func (o LoginProvider) String() string { 41 | return string(o) 42 | } 43 | -------------------------------------------------------------------------------- /models/system_config.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/mss-boot-io/mss-boot/pkg/config/source" 5 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 6 | ) 7 | 8 | /* 9 | * @Author: lwnmengjing 10 | * @Date: 2023/12/20 11:45:42 11 | * @Last Modified by: lwnmengjing 12 | * @Last Modified time: 2023/12/20 11:45:42 13 | */ 14 | 15 | type SystemConfig struct { 16 | actions.ModelGorm 17 | // Name 名称 18 | Name string `gorm:"column:name;size:128;index;default:'';not null" json:"name" binding:"required"` 19 | // Ext 扩展名 20 | Ext source.Scheme `gorm:"column:ext;size:16;default:'';not null" json:"ext" binding:"required"` 21 | // Content 内容 22 | Content string `gorm:"column:content;type:text" json:"content"` 23 | // remark 备注 24 | Remark string `gorm:"column:remark;size:255;default:'';not null" json:"remark"` 25 | // 内置配置 26 | BuiltIn bool `gorm:"->" json:"isBuiltIn"` 27 | } 28 | 29 | func (*SystemConfig) TableName() string { 30 | return "mss_boot_system_configs" 31 | } 32 | 33 | func (e *SystemConfig) GetExtend() source.Scheme { 34 | return e.Ext 35 | } 36 | 37 | // GenerateBytes generate bytes 38 | func (e *SystemConfig) GenerateBytes() ([]byte, error) { 39 | return []byte(e.Content), nil 40 | } 41 | -------------------------------------------------------------------------------- /cmd/migrate/migration/system/1691804837583_tables.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/mss-boot-io/mss-boot/pkg/migration" 7 | "gorm.io/gorm" 8 | 9 | "github.com/mss-boot-io/mss-boot-admin/models" 10 | ) 11 | 12 | func init() { 13 | _, fileName, _, _ := runtime.Caller(0) 14 | migration.Migrate.SetVersion(migration.GetFilename(fileName), _1691804837583Tables) 15 | } 16 | 17 | func _1691804837583Tables(db *gorm.DB, version string) error { 18 | return db.Transaction(func(tx *gorm.DB) error { 19 | 20 | err := tx.Migrator().AutoMigrate( 21 | new(models.Tenant), 22 | new(models.TenantDomain), 23 | new(models.AppConfig), 24 | new(models.SystemConfig), 25 | new(models.Role), 26 | new(models.User), 27 | new(models.UserConfig), 28 | new(models.UserOAuth2), 29 | new(models.Post), 30 | new(models.Department), 31 | new(models.API), 32 | new(models.Menu), 33 | new(models.Model), 34 | new(models.Field), 35 | new(models.Task), 36 | new(models.TaskRun), 37 | new(models.TaskRunLog), 38 | new(models.Language), 39 | new(models.Notice), 40 | new(models.Option), 41 | new(models.Statistics), 42 | ) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | return migration.Migrate.CreateVersion(tx, version) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /dto/monitor.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/shirou/gopsutil/v3/cpu" 5 | ) 6 | 7 | /* 8 | * @Author: lwnmengjing 9 | * @Date: 2024/3/23 23:45:37 10 | * @Last Modified by: lwnmengjing 11 | * @Last Modified time: 2024/3/23 23:45:37 12 | */ 13 | 14 | type MonitorResponse struct { 15 | // CPUPhysicalCore CPU物理核心数 16 | CPUPhysicalCore int `json:"cpuPhysicalCore"` 17 | // CPULogicalCore CPU逻辑核心数 18 | CPULogicalCore int `json:"cpuLogicalCore"` 19 | // CPUInfo CPU信息 20 | CPUInfo []MonitorCPUInfo `json:"cpuInfo"` 21 | // MemoryTotal 内存总量 22 | MemoryTotal uint64 `json:"memoryTotal"` 23 | // MemoryUsage 内存使用量 24 | MemoryUsage uint64 `json:"memoryUsage"` 25 | // MemoryUsagePercent 内存使用率 26 | MemoryUsagePercent float64 `json:"memoryUsagePercent"` 27 | // MemoryAvailable 内存可用量 28 | MemoryAvailable uint64 `json:"memoryAvailable"` 29 | // MemoryFree 内存空闲量 30 | MemoryFree uint64 `json:"memoryFree"` 31 | // DiskTotal 磁盘总量 32 | DiskTotal uint64 `json:"diskTotal"` 33 | // DiskUsage 磁盘使用量 34 | DiskUsage uint64 `json:"diskUsage"` 35 | // DiskUsagePercent 磁盘使用率 36 | DiskUsagePercent float64 `json:"diskUsagePercent"` 37 | } 38 | 39 | type MonitorCPUInfo struct { 40 | cpu.InfoStat 41 | // CPUUsagePercent CPU使用率 42 | CPUUsagePercent float64 `json:"cpuUsagePercent"` 43 | } 44 | -------------------------------------------------------------------------------- /dto/virtual.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/mss-boot-io/mss-boot-admin/pkg" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2024/1/2 17:35:45 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2024/1/2 17:35:45 10 | */ 11 | 12 | type ColumnType struct { 13 | Title string `json:"title"` 14 | DataIndex string `json:"dataIndex"` 15 | HideInForm bool `json:"hideInForm,omitempty"` 16 | HideInTable bool `json:"hideInTable,omitempty"` 17 | HideInDescriptions bool `json:"hideInDescriptions,omitempty"` 18 | ValueEnum map[string]ValueEnumType `json:"valueEnum,omitempty"` 19 | ValueType string `json:"valueType,omitempty"` 20 | ValidateRules []pkg.BaseRule `json:"validateRules,omitempty"` 21 | PK bool `json:"pk,omitempty"` 22 | } 23 | 24 | type ValueEnumType struct { 25 | Text string `json:"text"` 26 | Status string `json:"status"` 27 | Color string `json:"color,omitempty"` 28 | Disabled bool `json:"disabled,omitempty"` 29 | } 30 | 31 | type VirtualModelObject struct { 32 | Name string `json:"name"` 33 | Columns []*ColumnType `json:"columns"` 34 | } 35 | -------------------------------------------------------------------------------- /dto/menu.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/mss-boot-io/mss-boot/pkg/enum" 5 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 6 | ) 7 | 8 | /* 9 | * @Author: lwnmengjing 10 | * @Date: 2023/8/25 17:03:45 11 | * @Last Modified by: lwnmengjing 12 | * @Last Modified time: 2023/8/25 17:03:45 13 | */ 14 | 15 | type MenuSearch struct { 16 | actions.Pagination `search:"inline"` 17 | // ID id 18 | ID string `query:"id" form:"id" search:"type:contains;column:id"` 19 | // Name 名称 20 | Name string `query:"name" form:"name" search:"type:contains;column:name"` 21 | // Status 状态 22 | Status enum.Status `query:"status" form:"status" search:"type:exact;column:status"` 23 | // ParentID 父级id 24 | ParentID string `query:"parentID" form:"parentID" search:"-"` 25 | // Type 类型 26 | Type []string `query:"type[]" form:"type[]" search:"type:in;column:type"` 27 | // Show 是否显示 28 | Show bool `query:"show" form:"show" search:"type:exact;column:hide_in_menu"` 29 | } 30 | 31 | type GetAuthorizeRequest struct { 32 | RoleID string `uri:"roleID" binding:"required"` 33 | } 34 | 35 | type UpdateAuthorizeRequest struct { 36 | GetAuthorizeRequest 37 | Keys []string `json:"keys" binding:"required"` 38 | } 39 | 40 | type MenuBindAPIRequest struct { 41 | MenuID string `json:"menuID" binding:"required"` 42 | Paths []string `json:"paths" binding:"required"` 43 | } 44 | -------------------------------------------------------------------------------- /pkg/base_rule.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2024/1/4 14:47:51 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2024/1/4 14:47:51 8 | */ 9 | 10 | type BaseRule struct { 11 | ID string `json:"id"` 12 | WarningOnly bool `json:"warningOnly,omitempty"` 13 | Len uint8 `json:"len,omitempty"` 14 | Max uint8 `json:"max,omitempty"` 15 | Min uint8 `json:"min,omitempty"` 16 | Message string `json:"message,omitempty"` 17 | Pattern string `json:"pattern,omitempty"` 18 | Required bool `json:"required,omitempty"` 19 | Type RuleType `json:"type,omitempty"` 20 | Whitespace bool `json:"whitespace,omitempty"` 21 | ValidateTrigger string `json:"validateTrigger,omitempty"` 22 | } 23 | 24 | type RuleType string 25 | 26 | const ( 27 | RUleTypeString RuleType = "string" 28 | RUleTypeNumber RuleType = "number" 29 | RUleTypeBool RuleType = "boolean" 30 | RUleTypeMethod RuleType = "method" 31 | RUleTypeRegexp RuleType = "regexp" 32 | RUleTypeInt RuleType = "integer" 33 | RUleTypeFloat RuleType = "float" 34 | RUleTypeObject RuleType = "object" 35 | RUleTypeEnum RuleType = "enum" 36 | RUleTypeDate RuleType = "date" 37 | RUleTypeUrl RuleType = "url" 38 | RUleTypeHex RuleType = "hex" 39 | RUleTypeEmail RuleType = "email" 40 | ) 41 | -------------------------------------------------------------------------------- /models/api.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/mss-boot-io/mss-boot/pkg/config/gormdb" 10 | ) 11 | 12 | /* 13 | * @Author: lwnmengjing 14 | * @Date: 2023/8/14 08:41:16 15 | * @Last Modified by: lwnmengjing 16 | * @Last Modified time: 2023/8/14 08:41:16 17 | */ 18 | 19 | type API struct { 20 | actions.ModelGorm 21 | Name string `json:"name"` 22 | Path string `json:"path"` 23 | Method string `json:"method"` 24 | Handler string `json:"handler"` 25 | History bool `json:"history"` 26 | } 27 | 28 | func (*API) TableName() string { 29 | return "mss_boot_api" 30 | } 31 | 32 | func SaveAPI(routes gin.RoutesInfo) error { 33 | list := make([]*API, 0) 34 | for i := range routes { 35 | api := &API{ 36 | Name: routes[i].Path, 37 | Method: routes[i].Method, 38 | Handler: routes[i].Handler, 39 | } 40 | ps := strings.Split(routes[i].Path, "/") 41 | for j := range ps { 42 | if strings.HasPrefix(ps[j], ":") { 43 | ps[j] = "*" 44 | continue 45 | } 46 | if strings.HasPrefix(ps[j], "*") { 47 | ps[j] = "*" 48 | continue 49 | } 50 | } 51 | api.Path = strings.Join(ps, "/") 52 | gormdb.DB.Where("path = ? and method = ?", api.Path, api.Method).First(api) 53 | list = append(list, api) 54 | } 55 | return gormdb.DB.Save(&list).Error 56 | } 57 | -------------------------------------------------------------------------------- /.github/prompts/create-api.prompt.md: -------------------------------------------------------------------------------- 1 | ````prompt 2 | --- 3 | mode: agent 4 | model: GPT-4o 5 | tools: ['search', 'edit', 'fetch'] 6 | description: '生成基于mss-boot框架的RESTful API控制器代码' 7 | --- 8 | 9 | 你是一个经验丰富的Go语言开发者,精通mss-boot框架和Gin框架。你的任务是根据用户提供的API描述,生成符合项目规范的控制器(Controller)代码。 10 | 11 | ## Controller 开发模式 12 | 13 | ### 1. 标准CRUD控制器 14 | 15 | 使用 `controller.Simple` 快速构建标准的 RESTful API: 16 | 17 | ```go 18 | package apis 19 | 20 | import ( 21 | "github.com/gin-gonic/gin" 22 | "github.com/mss-boot-io/mss-boot-admin/center" 23 | "github.com/mss-boot-io/mss-boot-admin/dto" 24 | "github.com/mss-boot-io/mss-boot-admin/models" 25 | "github.com/mss-boot-io/mss-boot/pkg/response" 26 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 27 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 28 | ) 29 | 30 | func init() { 31 | e := &YourController{ 32 | Simple: controller.NewSimple( 33 | controller.WithAuth(true), // 启用JWT认证 34 | controller.WithModel(new(models.YourModel)), // 关联数据模型 35 | controller.WithSearch(new(dto.YourModelSearch)), // 关联搜索DTO 36 | controller.WithModelProvider(actions.ModelProviderGorm), // 使用GORM作为数据提供者 37 | controller.WithScope(center.Default.Scope), // 应用数据权限作用域 38 | ), 39 | } 40 | response.AppendController(e) 41 | } 42 | 43 | type YourController struct { 44 | *controller.Simple 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /apis/monitor.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/mss-boot-io/mss-boot-admin/service" 8 | "github.com/mss-boot-io/mss-boot/pkg/response" 9 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 10 | ) 11 | 12 | /* 13 | * @Author: lwnmengjing 14 | * @Date: 2024/3/23 23:40:31 15 | * @Last Modified by: lwnmengjing 16 | * @Last Modified time: 2024/3/23 23:40:31 17 | */ 18 | 19 | func init() { 20 | e := &Monitor{ 21 | Simple: controller.NewSimple(), 22 | } 23 | response.AppendController(e) 24 | } 25 | 26 | type Monitor struct { 27 | *controller.Simple 28 | service service.Monitor 29 | } 30 | 31 | func (e *Monitor) GetAction(string) response.Action { 32 | return nil 33 | } 34 | 35 | func (e *Monitor) Other(r *gin.RouterGroup) { 36 | r.GET("/monitor", response.AuthHandler, e.Monitor) 37 | } 38 | 39 | // Monitor 获取监控信息 40 | // @Summary 获取监控信息 41 | // @Description 获取监控信息 42 | // @Tags monitor 43 | // @Accept application/json 44 | // @Product application/json 45 | // @Success 200 {object} dto.MonitorResponse 46 | // @Router /admin/api/monitor [get] 47 | // @Security Bearer 48 | func (e *Monitor) Monitor(ctx *gin.Context) { 49 | api := response.Make(ctx) 50 | resp, err := e.service.Monitor(ctx) 51 | if err != nil { 52 | api.AddError(err).Log.Error("get monitor error") 53 | api.Err(http.StatusInternalServerError) 54 | return 55 | } 56 | api.OK(resp) 57 | } 58 | -------------------------------------------------------------------------------- /apis/ws.go: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lwnmengjing 3 | * @Date: 2024/5/1 15:06:37 4 | * @Last Modified by: lwnmengjing 5 | * @Last Modified time: 2024/5/1 15:06:37 6 | */ 7 | 8 | package apis 9 | 10 | import ( 11 | "github.com/gin-gonic/gin" 12 | "github.com/gorilla/websocket" 13 | "github.com/mss-boot-io/mss-boot/pkg/response" 14 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 15 | "net/http" 16 | ) 17 | 18 | func init() { 19 | e := &WS{ 20 | Simple: controller.NewSimple(), 21 | } 22 | response.AppendController(e) 23 | } 24 | 25 | var upgrader = websocket.Upgrader{ 26 | ReadBufferSize: 1024, 27 | WriteBufferSize: 1024, 28 | Subprotocols: []string{ 29 | "Sec-WebSocket-Extensions", 30 | }, 31 | CheckOrigin: func(r *http.Request) bool { 32 | return true 33 | }, 34 | } 35 | 36 | type WS struct { 37 | *controller.Simple 38 | } 39 | 40 | func (e *WS) GetAction(_ string) response.Action { 41 | return nil 42 | } 43 | 44 | func (e *WS) Other(r *gin.RouterGroup) { 45 | r.GET("/ws/event", e.Event) 46 | } 47 | 48 | // Event 长连接事件 49 | func (e *WS) Event(ctx *gin.Context) { 50 | api := response.Make(ctx) 51 | conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) 52 | if err != nil { 53 | api.AddError(err).Log.Error("websocket upgrade error") 54 | api.Err(http.StatusInternalServerError) 55 | return 56 | } 57 | err = conn.WriteJSON(gin.H{ 58 | "code": 200, 59 | }) 60 | if err != nil { 61 | api.AddError(err).Log.Error("websocket write error") 62 | } 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | ## How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Please also note any relevant details for your test configuration. 19 | 20 | - [ ] Test A 21 | - [ ] Test B 22 | 23 | **Test Configuration**: 24 | * Firmware version: 25 | * Hardware: 26 | * Toolchain: 27 | * SDK: 28 | 29 | ## Checklist: 30 | 31 | - [ ] My code follows the style guidelines of this project 32 | - [ ] I have performed a self-review of my own code 33 | - [ ] I have commented my code, particularly in hard-to-understand areas 34 | - [ ] I have made corresponding changes to the documentation 35 | - [ ] My changes generate no new warnings 36 | - [ ] I have added tests that prove my fix is effective or that my feature works 37 | - [ ] New and existing unit tests pass locally with my changes 38 | - [ ] Any dependent changes have been merged and published in downstream modules 39 | -------------------------------------------------------------------------------- /models/notice.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | /* 8 | * @Author: lwnmengjing 9 | * @Date: 2023/12/18 23:50:18 10 | * @Last Modified by: lwnmengjing 11 | * @Last Modified time: 2023/12/18 23:50:18 12 | */ 13 | 14 | type NoticeType string 15 | 16 | const ( 17 | NoticeTypeNotification NoticeType = "notification" 18 | NoticeTypeMessage NoticeType = "message" 19 | NoticeTypeEvent NoticeType = "event" 20 | NoticeTypeMail NoticeType = "mail" 21 | ) 22 | 23 | func (e NoticeType) String() string { 24 | return string(e) 25 | } 26 | 27 | type Notice struct { 28 | ModelGormTenant 29 | UserID string `json:"userID" gorm:"column:user_id;type:varchar(64)"` 30 | Title string `json:"title" gorm:"column:title;type:varchar(255)"` 31 | Key string `json:"key" gorm:"column:key;type:varchar(255)"` 32 | Read bool `json:"read" gorm:"column:read;size:1"` 33 | Avatar string `json:"avatar" gorm:"column:avatar;type:varchar(255)"` 34 | Extra string `json:"extra" gorm:"column:extra;type:varchar(255)"` 35 | Status string `json:"status" gorm:"column:status;size:10"` 36 | Description string `json:"description" gorm:"column:description;type:text"` 37 | Datetime *time.Time `json:"datetime" gorm:"column:datetime"` 38 | Type NoticeType `json:"type" gorm:"column:type;type:varchar(20)"` 39 | } 40 | 41 | func (e *Notice) TableName() string { 42 | return "mss_boot_notices" 43 | } 44 | -------------------------------------------------------------------------------- /notice/email/register_verify_code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Verification Code 6 | 35 | 36 | 37 |
38 |

Register Verification Code

39 |

Dear User,

40 |

Your verification code for register is:

41 |
{{ .Code }}
42 |

Please use this code to complete your register. The code is valid for 10 minutes.

43 |

If you did not request this code, please ignore this email.

44 | 47 |
48 | 49 | -------------------------------------------------------------------------------- /notice/email/password_reset_code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Password Reset Code 6 | 35 | 36 | 37 |
38 |

Password Reset Code

39 |

Dear User,

40 |

You have requested to reset your password. Your password reset code is:

41 |
{{ .Code }}
42 |

Please use this code to reset your password. The code is valid for 15 minutes.

43 |

If you did not request this, please ignore this email.

44 | 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /notice/email/login_verify_code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Verification Code 6 | 35 | 36 | 37 |
38 |

Login Verification Code

39 |

Dear User,

40 |

Your verification code for login is:

41 |
{{ .Code }}
42 |

Please use this code to complete your login. The code is valid for 10 minutes.

43 |

If you did not request this code, please ignore this email.

44 | 47 |
48 | 49 | -------------------------------------------------------------------------------- /cmd/cobra.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/mss-boot-io/mss-boot-admin/cmd/migrate" 11 | "github.com/mss-boot-io/mss-boot-admin/cmd/server" 12 | "github.com/mss-boot-io/mss-boot-admin/pkg" 13 | ) 14 | 15 | /* 16 | * @Author: lwnmengjing 17 | * @Date: 2023/8/10 00:14:22 18 | * @Last Modified by: lwnmengjing 19 | * @Last Modified time: 2023/8/10 00:14:22 20 | */ 21 | 22 | var rootCmd = &cobra.Command{ 23 | Use: "mss-boot-admin", 24 | Short: "mss-boot-admin", 25 | SilenceUsage: true, 26 | Long: `mss-boot-admin is a background management system developed by the mss-boot framework`, 27 | Args: func(cmd *cobra.Command, args []string) error { 28 | if len(args) < 1 { 29 | tip() 30 | return errors.New(pkg.Red("requires at least one arg")) 31 | } 32 | return nil 33 | }, 34 | PersistentPreRunE: func(*cobra.Command, []string) error { return nil }, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | tip() 37 | }, 38 | } 39 | 40 | func tip() { 41 | usageStr := `欢迎使用 ` + pkg.Green(`mss-boot-admin `+pkg.Version) + ` 可以使用 ` + pkg.Red(`-h`) + ` 查看命令` 42 | usageStr1 := `也可以参考 https://docs.mss-boot-io.top 的相关内容` 43 | fmt.Printf("%s\n", usageStr) 44 | fmt.Printf("%s\n", usageStr1) 45 | } 46 | 47 | func init() { 48 | rootCmd.AddCommand(server.StartCmd) 49 | rootCmd.AddCommand(migrate.StartCmd) 50 | } 51 | 52 | // Execute : apply commands 53 | func Execute() { 54 | if err := rootCmd.Execute(); err != nil { 55 | os.Exit(-2) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /apis/storage.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/mss-boot-io/mss-boot/pkg/response" 8 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 9 | 10 | "github.com/mss-boot-io/mss-boot-admin/middleware" 11 | "github.com/mss-boot-io/mss-boot-admin/service" 12 | ) 13 | 14 | /* 15 | * @Author: lwnmengjing 16 | * @Date: 2024/3/29 00:36:27 17 | * @Last Modified by: lwnmengjing 18 | * @Last Modified time: 2024/3/29 00:36:27 19 | */ 20 | 21 | func init() { 22 | e := &Storage{ 23 | Simple: controller.NewSimple(), 24 | } 25 | response.AppendController(e) 26 | } 27 | 28 | type Storage struct { 29 | *controller.Simple 30 | service service.Storage 31 | } 32 | 33 | func (*Storage) GetKey() string { 34 | return "storage" 35 | } 36 | 37 | func (*Storage) GetAction(string) response.Action { 38 | return nil 39 | } 40 | 41 | func (e *Storage) Other(r *gin.RouterGroup) { 42 | r.POST("/storage/upload", middleware.Auth.MiddlewareFunc(), e.Upload) 43 | } 44 | 45 | func (e *Storage) Upload(ctx *gin.Context) { 46 | api := response.Make(ctx) 47 | verify := middleware.GetVerify(ctx) 48 | file, err := ctx.FormFile("file") 49 | if err != nil { 50 | api.AddError(err).Log.Error("FormFile error") 51 | api.Err(http.StatusInternalServerError) 52 | return 53 | } 54 | u, err := e.service.Upload(ctx, file, verify.GetTenantID(), verify.GetUserID()) 55 | if err != nil { 56 | api.AddError(err).Log.Error("upload error") 57 | api.Err(http.StatusInternalServerError) 58 | return 59 | } 60 | api.OK(u) 61 | } 62 | -------------------------------------------------------------------------------- /models/option.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "strings" 7 | 8 | "github.com/google/uuid" 9 | "github.com/mss-boot-io/mss-boot/pkg/enum" 10 | ) 11 | 12 | /* 13 | * @Author: lwnmengjing 14 | * @Date: 2024/1/1 11:57:51 15 | * @Last Modified by: lwnmengjing 16 | * @Last Modified time: 2024/1/1 11:57:51 17 | */ 18 | 19 | type OptionItem struct { 20 | ID string `json:"id"` 21 | Key string `json:"key"` 22 | Label string `json:"label"` 23 | Value string `json:"value"` 24 | Color string `json:"color"` 25 | Sort int `json:"sort"` 26 | } 27 | 28 | type OptionItems []*OptionItem 29 | 30 | func (o *OptionItems) Value() (driver.Value, error) { 31 | if o == nil { 32 | return nil, nil 33 | } 34 | for i := range *o { 35 | if (*o)[i].ID == "" { 36 | (*o)[i].ID = strings.ReplaceAll(uuid.New().String(), "-", "") 37 | } 38 | } 39 | return json.Marshal(o) 40 | } 41 | 42 | func (o *OptionItems) Scan(val any) error { 43 | return json.Unmarshal(val.([]uint8), o) 44 | } 45 | 46 | type Option struct { 47 | ModelGormTenant 48 | // Name 选项名称 49 | Name string `json:"name" gorm:"column:name;type:varchar(255);not null;unique_index:idx_name;comment:选项名称"` 50 | // Remark 备注 51 | Remark string `json:"remark" gorm:"column:remark;type:varchar(255);not null;comment:备注"` 52 | // Items 选项内容 53 | Items *OptionItems `json:"items" gorm:"column:items;type:json;comment:选项内容"` 54 | // Status 状态 55 | Status enum.Status `json:"status" gorm:"column:status;comment:状态;size:10"` 56 | } 57 | 58 | func (*Option) TableName() string { 59 | return "mss_boot_options" 60 | } 61 | -------------------------------------------------------------------------------- /pkg/textcolor.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import "fmt" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2023/8/10 00:14:22 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2023/8/10 00:14:22 10 | */ 11 | 12 | // 前景 背景 颜色 13 | // --------------------------------------- 14 | // 30 40 黑色 15 | // 31 41 红色 16 | // 32 42 绿色 17 | // 33 43 黄色 18 | // 34 44 蓝色 19 | // 35 45 紫红色 20 | // 36 46 青蓝色 21 | // 37 47 白色 22 | // 23 | // 代码 意义 24 | // ------------------------- 25 | // 0 终端默认设置 26 | // 1 高亮显示 27 | // 4 使用下划线 28 | // 5 闪烁 29 | // 7 反白显示 30 | // 8 不可见 31 | 32 | const ( 33 | TextBlack = iota + 30 34 | TextRed 35 | TextGreen 36 | TextYellow 37 | TextBlue 38 | TextMagenta 39 | TextCyan 40 | TextWhite 41 | ) 42 | 43 | func Black(msg string) string { 44 | return SetColor(msg, 0, 0, TextBlack) 45 | } 46 | 47 | func Red(msg string) string { 48 | return SetColor(msg, 0, 0, TextRed) 49 | } 50 | 51 | func Green(msg string) string { 52 | return SetColor(msg, 0, 0, TextGreen) 53 | } 54 | 55 | func Yellow(msg string) string { 56 | return SetColor(msg, 0, 0, TextYellow) 57 | } 58 | 59 | func Blue(msg string) string { 60 | return SetColor(msg, 0, 0, TextBlue) 61 | } 62 | 63 | func Magenta(msg string) string { 64 | return SetColor(msg, 0, 0, TextMagenta) 65 | } 66 | 67 | func Cyan(msg string) string { 68 | return SetColor(msg, 0, 0, TextCyan) 69 | } 70 | 71 | func White(msg string) string { 72 | return SetColor(msg, 0, 0, TextWhite) 73 | } 74 | 75 | func SetColor(msg string, conf, bg, text int) string { 76 | return fmt.Sprintf("%c[%d;%d;%dm%s%c[0m", 0x1B, conf, bg, text, msg, 0x1B) 77 | } 78 | -------------------------------------------------------------------------------- /apis/statistics.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/mss-boot-io/mss-boot/pkg/response" 8 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 9 | 10 | "github.com/mss-boot-io/mss-boot-admin/dto" 11 | "github.com/mss-boot-io/mss-boot-admin/service" 12 | ) 13 | 14 | /* 15 | * @Author: lwnmengjing 16 | * @Date: 2024/1/12 17:48:43 17 | * @Last Modified by: lwnmengjing 18 | * @Last Modified time: 2024/1/12 17:48:43 19 | */ 20 | 21 | func init() { 22 | e := &Statistics{ 23 | Simple: controller.NewSimple(), 24 | } 25 | response.AppendController(e) 26 | } 27 | 28 | type Statistics struct { 29 | *controller.Simple 30 | service service.Statistics 31 | } 32 | 33 | func (*Statistics) GetAction(string) response.Action { 34 | return nil 35 | } 36 | 37 | func (e *Statistics) Other(r *gin.RouterGroup) { 38 | r.GET("/statistics/:name", response.AuthHandler, e.Get) 39 | } 40 | 41 | // Get 获取统计 42 | // @Summary 获取统计 43 | // @Description 获取统计 44 | // @Tags statistics 45 | // @Accept application/json 46 | // @Product application/json 47 | // @Param name path string true "name" 48 | // @Success 200 {object} dto.StatisticsGetResponse 49 | // @Router /admin/api/statistics/{name} [get] 50 | // @Security Bearer 51 | func (e *Statistics) Get(ctx *gin.Context) { 52 | api := response.Make(ctx) 53 | req := &dto.StatisticsGetRequest{} 54 | if err := api.Bind(req).Error; err != nil { 55 | api.Err(http.StatusUnprocessableEntity) 56 | return 57 | } 58 | result, err := e.service.Get(ctx, req.Name) 59 | if err != nil { 60 | api.Err(http.StatusInternalServerError) 61 | return 62 | } 63 | api.OK(result) 64 | } 65 | -------------------------------------------------------------------------------- /compose/consul/config/server.hcl: -------------------------------------------------------------------------------- 1 | # Common Consul server configuration for production 2 | 3 | server = true 4 | bootstrap_expect = 3 5 | datacenter = "dc1" 6 | data_dir = "/consul/data" 7 | log_level = "INFO" 8 | 9 | # Bind to all interfaces inside container; advertise is auto on container IP 10 | bind_addr = "0.0.0.0" 11 | client_addr = "0.0.0.0" 12 | 13 | # Join peers by DNS names (services within the same docker network) 14 | retry_join = [ 15 | "consul-server-1", 16 | "consul-server-2", 17 | "consul-server-3", 18 | ] 19 | 20 | # Security hardening 21 | disable_remote_exec = true 22 | enable_script_checks = false 23 | 24 | # Enable service mesh (Connect) 25 | connect { 26 | enabled = true 27 | } 28 | 29 | # Autopilot for Raft 30 | autopilot { 31 | cleanup_dead_servers = true 32 | last_contact_threshold = "200ms" 33 | max_trailing_logs = 250 34 | server_stabilization_time = "10s" 35 | } 36 | 37 | # ACLs: enabled with deny-by-default. Bootstrap a management token after cluster is healthy. 38 | acl { 39 | enabled = true 40 | default_policy = "deny" 41 | down_policy = "extend-cache" 42 | enable_token_persistence = true 43 | } 44 | 45 | # TLS for HTTPS, gRPC, and internal RPC. Provide certs in ./certs. 46 | ca_file = "/consul/certs/ca.pem" 47 | cert_file = "/consul/certs/server.pem" 48 | key_file = "/consul/certs/server-key.pem" 49 | verify_incoming = true 50 | verify_outgoing = true 51 | verify_server_hostname = true 52 | 53 | ports { 54 | http = -1 # disable plaintext HTTP 55 | https = 8501 # enable HTTPS 56 | grpc = 8502 # gRPC (TLS) 57 | } 58 | 59 | # UI over HTTPS only 60 | ui_config { 61 | enabled = true 62 | } 63 | -------------------------------------------------------------------------------- /cmd/tools/pr/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/google/go-github/github" 11 | "github.com/spf13/cast" 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | func main() { 16 | // Authenticate with GitHub using a personal access token 17 | token := os.Getenv("GITHUB_TOKEN") 18 | ctx := context.Background() 19 | ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) 20 | tc := oauth2.NewClient(ctx, ts) 21 | client := github.NewClient(tc) 22 | 23 | f, err := os.Open(os.Getenv("COVERAGE_FILE")) 24 | if err != nil { 25 | fmt.Println("Error reading file:", err) 26 | os.Exit(-1) 27 | } 28 | scanner := bufio.NewScanner(f) 29 | var lastLine string 30 | for scanner.Scan() { 31 | lastLine = scanner.Text() // 最后一行是最后一个被扫描的行 32 | } 33 | 34 | if err = scanner.Err(); err != nil { 35 | fmt.Println("Error reading file:", err) 36 | os.Exit(-1) 37 | } 38 | 39 | content := ` 40 | | Field | Coverage | 41 | | ---- | -------- | 42 | ` + lastLine 43 | 44 | // Print or save the Markdown table. 45 | fmt.Println(content) 46 | 47 | // Create a comment with the coverage table and submit it to the PR 48 | repo := os.Getenv("REPO_NAME") // Set by GitHub Actions 49 | prNumber := os.Getenv("PR_NUMBER") // Set by GitHub Actions 50 | owner := strings.Split(repo, "/")[0] 51 | repo = strings.Split(repo, "/")[1] 52 | 53 | comment := &github.IssueComment{ 54 | Body: &content, 55 | } 56 | _, _, err = client.Issues.CreateComment(ctx, owner, repo, cast.ToInt(prNumber), comment) 57 | if err != nil { 58 | fmt.Println("Error creating comment:", err) 59 | os.Exit(-1) 60 | } 61 | 62 | fmt.Println("Comment submitted successfully!") 63 | } 64 | -------------------------------------------------------------------------------- /compose/consul/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "log/slog" 7 | "os" 8 | "time" 9 | 10 | consul "github.com/hashicorp/consul/api" 11 | ) 12 | 13 | func main() { 14 | // 创建Consul API客户端配置 15 | config := consul.DefaultConfig() 16 | config.Address = "localhost:8500" // Consul服务器的地址和端口 17 | 18 | // 创建Consul API客户端 19 | client, err := consul.NewClient(config) 20 | if err != nil { 21 | log.Fatalf("Error creating Consul client: %s", err) 22 | } 23 | 24 | rb, err := os.ReadFile("../../config/application.yml") 25 | if err != nil { 26 | log.Fatalf("Error reading file: %s", err) 27 | } 28 | key := "mss-boot-admin/config/application.yml" 29 | // 写入键值对配置 30 | pair := &consul.KVPair{ 31 | Key: key, 32 | Value: rb, 33 | } 34 | _, err = client.KV().Put(pair, nil) 35 | if err != nil { 36 | log.Fatalf("Error writing to Consul KV: %s", err) 37 | } 38 | fmt.Println("Successfully wrote to Consul KV") 39 | 40 | // 读取键值对配置 41 | readPair, _, err := client.KV().Get(key, nil) 42 | if err != nil { 43 | log.Fatalf("Error reading from Consul KV: %s", err) 44 | } 45 | 46 | // 如果找到键值对,则打印值 47 | if readPair != nil { 48 | fmt.Printf("Value for key %s: %s\n", key, string(readPair.Value)) 49 | } else { 50 | fmt.Printf("Key %s not found in Consul\n", key) 51 | } 52 | 53 | params := &consul.QueryOptions{WaitIndex: 0, WaitTime: 10 * time.Second} 54 | for { 55 | // 获取最新的配置变化 56 | pairs, meta, err := client.KV().List(key, params) 57 | if err != nil { 58 | slog.Error("Error watching for config changes", slog.Any("err", err)) 59 | continue 60 | } 61 | if pairs != nil { 62 | fmt.Printf("Config updated: %s\n", pair.Value) 63 | } 64 | params.WaitIndex = meta.LastIndex 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pkg/time.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import "time" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2024/1/12 16:52:04 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2024/1/12 16:52:04 10 | */ 11 | 12 | func NowFormatSecond() string { 13 | return TimeFormatSecond(time.Now()) 14 | } 15 | 16 | func NowFormatMinute() string { 17 | return TimeFormatMinute(time.Now()) 18 | } 19 | 20 | func NowFormatHour() string { 21 | return TimeFormatHour(time.Now()) 22 | } 23 | 24 | func NowFormatDay() string { 25 | return TimeFormatDay(time.Now()) 26 | } 27 | 28 | func NowFormatMonth() string { 29 | return TimeFormatMonth(time.Now()) 30 | } 31 | 32 | func NowFormatYear() string { 33 | return TimeFormatYear(time.Now()) 34 | } 35 | 36 | func TimeFormatSecond(t time.Time) string { 37 | return t.Format("2006-01-02 15:04:05") 38 | } 39 | 40 | func TimeFormatMinute(t time.Time) string { 41 | return t.Format("2006-01-02 15:04") 42 | } 43 | 44 | func TimeFormatHour(t time.Time) string { 45 | return t.Format("2006-01-02 15") 46 | } 47 | 48 | func TimeFormatDay(t time.Time) string { 49 | return t.Format("2006-01-02") 50 | } 51 | 52 | func TimeFormatMonth(t time.Time) string { 53 | return t.Format("2006-01") 54 | } 55 | 56 | func TimeFormatYear(t time.Time) string { 57 | return t.Format("2006") 58 | } 59 | 60 | func NowStartDay() time.Time { 61 | return TimeStartDay(time.Now()) 62 | } 63 | 64 | func NowEndDay() time.Time { 65 | return TimeEndDay(time.Now()) 66 | } 67 | 68 | func TimeStartDay(t time.Time) time.Time { 69 | return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) 70 | } 71 | 72 | func TimeEndDay(t time.Time) time.Time { 73 | return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location()) 74 | } 75 | -------------------------------------------------------------------------------- /cmd/tools/pr/go.sum: -------------------------------------------------------------------------------- 1 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 2 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 3 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 4 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 5 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 6 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 7 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 8 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 9 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 10 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 11 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 12 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 13 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 14 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 15 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 16 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 17 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 18 | golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= 19 | golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= 20 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 21 | -------------------------------------------------------------------------------- /config/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | addr: 0.0.0.0:8080 3 | metrics: true 4 | healthz: true 5 | readyz: true 6 | pprof: true 7 | application: 8 | mode: dev 9 | origin: http://127.0.0.1:8080 10 | # ui: 11 | # enabled: true 12 | # addr: 0.0.0.0:8000 13 | # path: dist 14 | staticPath: 15 | /public: public 16 | labels: 17 | app: mss-boot-admin 18 | namespace: local 19 | cluster: local 20 | logger: 21 | # 日志存放路径,关闭控制台日志后,日志文件存放位置 22 | # path: temp/logs 23 | # 日志输出,file:文件,default:命令行,其他:命令行 loki: 推送到loki 24 | stdout: default #控制台日志,启用后,不输出到文件 25 | # 日志等级, trace, debug, info, warn, error, fatal 26 | level: info 27 | # 日志格式 json json格式 28 | json: false 29 | addSource: true 30 | # loki: 31 | # url: http://loki:3100 32 | # interval: 5s 33 | database: 34 | driver: sqlite 35 | source: 'mss-boot-admin-local.db' 36 | name: mss-boot-admin-local 37 | config: 38 | disableForeignKeyConstraintWhenMigrating: true 39 | casbinModel: | 40 | [request_definition] 41 | r = sub, tp, obj, act 42 | 43 | [policy_definition] 44 | p = sub, tp, obj, act 45 | 46 | [policy_effect] 47 | e = some(where (p.eft == allow)) 48 | 49 | [matchers] 50 | m = r.sub == p.sub && r.tp == p.tp && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act) 51 | timeout: 10s 52 | auth: 53 | realm: 'mss-boot-admin zone' 54 | key: 'mss-boot-admin-secret' 55 | timeout: '12h' 56 | maxRefresh: '2160h' 57 | identityKey: 'mss-boot-admin-identity-key' 58 | pyroscope: 59 | enabled: false 60 | applicationName: mss-boot-admin 61 | serverAddress: http://pyroscope:4040 62 | cache: 63 | queryCache: false 64 | queryCacheDuration: 1h 65 | queryCacheKeys: 66 | - '*' 67 | memory: '' 68 | # redis: 69 | # addr: '127.0.0.1:6379' 70 | queue: 71 | memory: 72 | poolSize: 10 -------------------------------------------------------------------------------- /.github/workflows/swagger.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages 2 | name: Swagger 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: 8 | - main 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 20 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 21 | concurrency: 22 | group: "pages" 23 | cancel-in-progress: false 24 | 25 | jobs: 26 | # Build job 27 | build: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | - name: Setup golang 33 | uses: actions/setup-go@v5 34 | with: 35 | go-version: 1.25 36 | - name: Install swag 37 | run: go install github.com/swaggo/swag/cmd/swag@latest 38 | 39 | - name: Setup Pages 40 | uses: actions/configure-pages@v5 41 | 42 | - name: Generate 43 | run: go generate ./... 44 | 45 | - name: Delete Redundant Files 46 | run: rm -rf docs/docs.go 47 | - name: Upload artifact 48 | uses: actions/upload-pages-artifact@v3 49 | with: 50 | path: ./docs 51 | 52 | # Deployment job 53 | deploy: 54 | environment: 55 | name: github-pages 56 | url: ${{ steps.deployment.outputs.page_url }} 57 | runs-on: ubuntu-latest 58 | needs: build 59 | steps: 60 | - name: Deploy to GitHub Pages 61 | id: deployment 62 | uses: actions/deploy-pages@v4 63 | -------------------------------------------------------------------------------- /dto/template.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type TemplateGetBranchesReq struct { 4 | Source string `query:"source" form:"source" binding:"required"` 5 | AccessToken string `query:"accessToken" form:"accessToken"` 6 | } 7 | 8 | type TemplateGetBranchesResp struct { 9 | Branches []string `json:"branches"` 10 | } 11 | 12 | type TemplateGetPathReq struct { 13 | Source string `query:"source" form:"source" binding:"required"` 14 | Branch string `query:"branch" form:"branch"` 15 | AccessToken string `query:"accessToken" form:"accessToken"` 16 | } 17 | 18 | type TemplateGetPathResp struct { 19 | Path []string `json:"path"` 20 | } 21 | 22 | type TemplateGetParamsReq struct { 23 | Source string `query:"source" form:"source" binding:"required"` 24 | Branch string `query:"branch" form:"branch"` 25 | Path string `query:"path" form:"path"` 26 | AccessToken string `query:"accessToken" form:"accessToken"` 27 | } 28 | 29 | type TemplateGetParamsResp struct { 30 | Params []TemplateParam `json:"params"` 31 | } 32 | 33 | type TemplateParam struct { 34 | Name string `json:"name"` 35 | Tip string `json:"tip"` 36 | } 37 | 38 | type TemplateGenerateReq struct { 39 | Template TemplateParams `json:"template"` 40 | Generate GenerateParams `json:"generate"` 41 | AccessToken string `query:"accessToken" form:"accessToken"` 42 | Email string `query:"email" form:"email"` 43 | } 44 | 45 | type TemplateParams struct { 46 | Source string `json:"source" binding:"required"` 47 | Branch string `json:"branch"` 48 | Path string `json:"path"` 49 | } 50 | 51 | type GenerateParams struct { 52 | Service string `json:"service"` 53 | Repo string `json:"repo" binding:"required"` 54 | Params map[string]string `json:"params"` 55 | } 56 | 57 | type TemplateGenerateResp struct { 58 | Repo string `json:"repo"` 59 | Branch string `json:"branch"` 60 | } 61 | -------------------------------------------------------------------------------- /models/language.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "strings" 7 | 8 | "github.com/google/uuid" 9 | "github.com/mss-boot-io/mss-boot/pkg/enum" 10 | ) 11 | 12 | /* 13 | * @Author: lwnmengjing 14 | * @Date: 2023/12/12 11:38:05 15 | * @Last Modified by: lwnmengjing 16 | * @Last Modified time: 2023/12/12 11:38:05 17 | */ 18 | 19 | type LanguageDefine struct { 20 | // ID 主键 21 | ID string `json:"id"` 22 | // Group 分组 23 | Group string `json:"group" gorm:"column:group;comment:分组;type:varchar(20);not null" binding:"required"` 24 | // Key 键 25 | Key string `json:"key" gorm:"column:key;comment:键;type:varchar(20);not null" binding:"required"` 26 | // Value 值 27 | Value string `json:"value" gorm:"column:value;comment:值;type:varchar(100);not null" binding:"required"` 28 | } 29 | 30 | type Language struct { 31 | ModelGormTenant 32 | // Name 名称 33 | Name string `json:"name" gorm:"column:name;comment:名称;type:varchar(255);not null" binding:"required"` 34 | // Remark 备注 35 | Remark string `json:"remark" gorm:"column:remark;comment:备注;type:varchar(255);not null"` 36 | // Statue 状态 37 | Status enum.Status `json:"status" gorm:"column:status;comment:状态;size:10"` 38 | // Defines 39 | Defines *LanguageDefines `json:"defines,omitempty" gorm:"column:defines;comment:定义;type:json"` 40 | } 41 | 42 | func (*Language) TableName() string { 43 | return "mss_boot_languages" 44 | } 45 | 46 | type LanguageDefines []*LanguageDefine 47 | 48 | func (l *LanguageDefines) Scan(val any) error { 49 | return json.Unmarshal(val.([]uint8), l) 50 | } 51 | 52 | func (l *LanguageDefines) Value() (driver.Value, error) { 53 | if l == nil { 54 | return nil, nil 55 | } 56 | for i := range *l { 57 | if (*l)[i].ID == "" { 58 | (*l)[i].ID = strings.ReplaceAll(uuid.New().String(), "-", "") 59 | } 60 | } 61 | return json.Marshal(*l) 62 | } 63 | -------------------------------------------------------------------------------- /models/task_run.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/mss-boot-io/mss-boot/pkg/config/gormdb" 8 | 9 | "github.com/google/uuid" 10 | "github.com/mss-boot-io/mss-boot/pkg/enum" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | /* 15 | * @Author: lwnmengjing 16 | * @Date: 2023/12/5 19:42:35 17 | * @Last Modified by: lwnmengjing 18 | * @Last Modified time: 2023/12/5 19:42:35 19 | */ 20 | 21 | // TaskRun support http/grpc/script status: 0: unknown, 1: success, 2: fail, 3: running 22 | type TaskRun struct { 23 | ID string `gorm:"primarykey" json:"id" form:"id" query:"id"` 24 | CreatedAt time.Time `json:"createdAt"` 25 | TaskID string `json:"taskID" gorm:"index;foreignKey:TaskID;references:ID"` 26 | Status enum.Status `json:"status" gorm:"size:10"` 27 | } 28 | 29 | func (*TaskRun) TableName() string { 30 | return "mss_boot_task_runs" 31 | } 32 | 33 | func (t *TaskRun) BeforeCreate(_ *gorm.DB) (err error) { 34 | t.ID = strings.ReplaceAll(uuid.New().String(), "-", "") 35 | return nil 36 | } 37 | 38 | func (t *TaskRun) Write(p []byte) (int, error) { 39 | log := &TaskRunLog{ 40 | TaskRunID: t.ID, 41 | Content: string(p), 42 | } 43 | err := gormdb.DB.Create(log).Error 44 | if err != nil { 45 | return 0, err 46 | } 47 | return len(p), nil 48 | } 49 | 50 | type TaskRunLog struct { 51 | ID string `gorm:"primarykey" json:"id" form:"id" query:"id"` 52 | TaskRunID string `json:"taskRunID" gorm:"index;foreignKey:TaskRunID;references:ID"` 53 | CreatedAt time.Time `json:"createdAt"` 54 | Content string `json:"content" gorm:"type:text"` 55 | } 56 | 57 | func (*TaskRunLog) TableName() string { 58 | return "mss_boot_task_run_logs" 59 | } 60 | 61 | func (l *TaskRunLog) BeforeCreate(_ *gorm.DB) (err error) { 62 | l.ID = strings.ReplaceAll(uuid.New().String(), "-", "") 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /models/user_auth_token.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/mss-boot-io/mss-boot-admin/center" 6 | "github.com/mss-boot-io/mss-boot-admin/pkg" 7 | "time" 8 | 9 | "github.com/mss-boot-io/mss-boot/pkg/security" 10 | 11 | "github.com/mss-boot-io/mss-boot-admin/middleware" 12 | ) 13 | 14 | /* 15 | * @Author: lwnmengjing 16 | * @Date: 2024/7/30 09:45:03 17 | * @Last Modified by: lwnmengjing 18 | * @Last Modified time: 2024/7/30 09:45:03 19 | */ 20 | 21 | type UserAuthToken struct { 22 | ModelGormTenant 23 | UserID string `gorm:"type:varchar(64);index;comment:用户ID" json:"userID"` 24 | Token string `gorm:"type:text;comment:token" json:"token"` 25 | ExpiredAt time.Time `gorm:"index;comment:过期时间" json:"expiredAt"` 26 | Revoked bool `gorm:"index;comment:是否撤销" json:"revoked"` 27 | } 28 | 29 | func (*UserAuthToken) TableName() string { 30 | return "mss_boot_user_auth_token" 31 | } 32 | 33 | // GenerateUserAuthToken 生成用户令牌 34 | func GenerateUserAuthToken(ctx *gin.Context, verify security.Verifier, validityPeriod time.Duration) (*UserAuthToken, error) { 35 | if validityPeriod <= 0 { 36 | validityPeriod = 100 * 12 * 30 * 24 * time.Hour 37 | } 38 | auth := *middleware.Auth 39 | auth.Timeout = validityPeriod 40 | auth.TimeoutFunc = func(_ interface{}) time.Duration { 41 | return validityPeriod 42 | } 43 | userAuthToken := &UserAuthToken{ 44 | UserID: verify.GetUserID(), 45 | } 46 | userAuthToken.ID = pkg.SimpleID() 47 | verify.SetRefreshTokenDisable(true) 48 | verify.SetPersonAccessToken(userAuthToken.ID) 49 | var err error 50 | userAuthToken.Token, userAuthToken.ExpiredAt, err = auth.TokenGenerator(verify) 51 | if err != nil { 52 | return nil, err 53 | } 54 | err = center.GetDB(ctx, &UserAuthToken{}).Create(userAuthToken).Error 55 | if err != nil { 56 | return nil, err 57 | } 58 | return userAuthToken, nil 59 | } 60 | -------------------------------------------------------------------------------- /service/user_config.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/mss-boot-io/mss-boot-admin/center" 8 | "github.com/mss-boot-io/mss-boot-admin/models" 9 | "github.com/spf13/cast" 10 | ) 11 | 12 | /* 13 | * @Author: lwnmengjing 14 | * @Date: 2024/3/2 00:42:39 15 | * @Last Modified by: lwnmengjing 16 | * @Last Modified time: 2024/3/2 00:42:39 17 | */ 18 | 19 | type UserConfig struct{} 20 | 21 | func (e *UserConfig) Profile(ctx *gin.Context, tenantID, userID string) (map[string]gin.H, error) { 22 | list := make([]*models.UserConfig, 0) 23 | err := center.GetDB(ctx, &models.UserConfig{}). 24 | Where("tenant_id = ?", tenantID). 25 | Where("user_id = ?", userID). 26 | Find(&list).Error 27 | if err != nil { 28 | return nil, err 29 | } 30 | result := make(map[string]gin.H) 31 | for i := range list { 32 | if _, ok := result[list[i].Group]; !ok { 33 | result[list[i].Group] = make(gin.H) 34 | } 35 | result[list[i].Group][list[i].Name] = list[i].Value 36 | } 37 | return result, nil 38 | } 39 | 40 | func (e *UserConfig) Group(ctx *gin.Context, userID, group string) (map[string]string, error) { 41 | list := make([]*models.UserConfig, 0) 42 | err := center.GetTenant().GetDB(ctx, &models.UserConfig{}). 43 | Where(models.UserConfig{UserID: userID, Group: group}). 44 | Find(&list).Error 45 | if err != nil { 46 | return nil, err 47 | } 48 | result := make(map[string]string) 49 | for i := range list { 50 | result[list[i].Name] = list[i].Value 51 | } 52 | return result, nil 53 | } 54 | 55 | func (e *UserConfig) CreateOrUpdate(ctx *gin.Context, userID, group string, data map[string]any) error { 56 | var err error 57 | for k, v := range data { 58 | err = center.GetUserConfig().SetUserConfig(ctx, userID, fmt.Sprintf("%s.%s", group, k), cast.ToString(v)) 59 | if err != nil { 60 | return err 61 | } 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /compose/kafka/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/segmentio/kafka-go" 10 | ) 11 | 12 | /* 13 | * @Author: lwnmengjing 14 | * @Date: 2024/3/13 18:47:32 15 | * @Last Modified by: lwnmengjing 16 | * @Last Modified time: 2024/3/13 18:47:32 17 | */ 18 | 19 | func main() { 20 | Product() 21 | fmt.Println("********************************") 22 | Consumer() 23 | } 24 | 25 | func Product() { 26 | // to produce messages 27 | topic := "my-topic" 28 | partition := 0 29 | 30 | conn, err := kafka.Dial("tcp", "localhost:9092") 31 | 32 | //conn, err := kafka.DialLeader(context.Background(), "tcp", "localhost:9092", topic, partition) 33 | if err != nil { 34 | log.Fatal("failed to dial leader:", err) 35 | } 36 | 37 | _ = conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) 38 | _, err = conn.WriteMessages( 39 | kafka.Message{Topic: topic, Partition: partition, Value: []byte("one!")}, 40 | kafka.Message{Topic: topic, Partition: partition, Value: []byte("two!")}, 41 | kafka.Message{Topic: topic, Partition: partition, Value: []byte("three!")}, 42 | ) 43 | if err != nil { 44 | log.Fatal("failed to write messages:", err) 45 | } 46 | 47 | if err := conn.Close(); err != nil { 48 | log.Fatal("failed to close writer:", err) 49 | } 50 | } 51 | 52 | func Consumer() { 53 | // to consume messages 54 | topic := "my-topic" 55 | partition := 0 56 | 57 | // make a new reader that consumes from topic-A, partition 0, at offset 42 58 | r := kafka.NewReader(kafka.ReaderConfig{ 59 | Brokers: []string{"localhost:9092"}, 60 | Topic: topic, 61 | Partition: partition, 62 | MaxBytes: 10e6, // 10MB 63 | GroupID: "0", 64 | }) 65 | 66 | for { 67 | m, err := r.ReadMessage(context.Background()) 68 | if err != nil { 69 | break 70 | } 71 | fmt.Printf("message at offset %d: %s = %s\n", m.Offset, string(m.Key), string(m.Value)) 72 | } 73 | 74 | if err := r.Close(); err != nil { 75 | log.Fatal("failed to close reader:", err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /cmd/migrate/migration/make.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2023/8/12 09:15:17 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2023/8/12 09:15:17 8 | */ 9 | // 10 | //import ( 11 | // "fmt" 12 | // "log/slog" 13 | // "os" 14 | // "path/filepath" 15 | // "sort" 16 | // "strconv" 17 | // "sync" 18 | // 19 | // "github.com/spf13/cast" 20 | // "gorm.io/gorm" 21 | //) 22 | // 23 | //var Migrate = &Migration{ 24 | // version: make(map[int]func(db *gorm.DB, version string) error), 25 | //} 26 | // 27 | //type Migration struct { 28 | // db *gorm.DB 29 | // version map[int]func(db *gorm.DB, version string) error 30 | // mutex sync.Mutex 31 | //} 32 | // 33 | //func (e *Migration) GetDb() *gorm.DB { 34 | // return e.db 35 | //} 36 | // 37 | //func (e *Migration) SetDb(db *gorm.DB) { 38 | // e.db = db 39 | //} 40 | // 41 | //func (e *Migration) SetVersion(k int, f func(db *gorm.DB, version string) error) { 42 | // e.mutex.Lock() 43 | // defer e.mutex.Unlock() 44 | // e.version[k] = f 45 | //} 46 | // 47 | //func (e *Migration) Migrate() { 48 | // versions := make([]int, 0) 49 | // for k := range e.version { 50 | // versions = append(versions, k) 51 | // } 52 | // if !sort.IntsAreSorted(versions) { 53 | // sort.Ints(versions) 54 | // } 55 | // var err error 56 | // var count int64 57 | // for _, v := range versions { 58 | // err = e.db.Table("mss_boot_migration").Where("version = ?", fmt.Sprintf("%d", v)).Count(&count).Error 59 | // if err != nil { 60 | // slog.Error("get migration version error", "err", err) 61 | // os.Exit(-1) 62 | // } 63 | // if count > 0 { 64 | // slog.Info("migration version already exists", "version", v) 65 | // count = 0 66 | // continue 67 | // } 68 | // err = (e.version[v])(e.db, strconv.Itoa(v)) 69 | // if err != nil { 70 | // slog.Error("migrate version error", "version", v, "err", err) 71 | // os.Exit(-1) 72 | // } 73 | // } 74 | //} 75 | // 76 | //func GetFilename(s string) int { 77 | // s = filepath.Base(s) 78 | // return cast.ToInt(s[:13]) 79 | //} 80 | -------------------------------------------------------------------------------- /dto/github.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/mss-boot-io/mss-boot-admin/pkg" 5 | "time" 6 | ) 7 | 8 | /* 9 | * @Author: lwnmengjing 10 | * @Date: 2022/10/19 16:43:12 11 | * @Last Modified by: lwnmengjing 12 | * @Last Modified time: 2022/10/19 16:43:12 13 | */ 14 | 15 | type OauthGetLoginURLReq struct { 16 | State string `query:"state" form:"state" binding:"required"` 17 | } 18 | 19 | type OauthCallbackReq struct { 20 | Provider pkg.LoginProvider `uri:"provider" binding:"required"` 21 | Code string `query:"code" form:"code" binding:"required"` 22 | State string `query:"state" form:"state" binding:"required"` 23 | } 24 | 25 | type GithubControlReq struct { 26 | //github密码或者token 27 | Password string `json:"password" binding:"required"` 28 | } 29 | 30 | type GithubGetResp struct { 31 | //github邮箱 32 | Email string `json:"email" bson:"email"` 33 | //已配置 34 | Configured bool `json:"configured" bson:"configured"` 35 | //创建时间 36 | CreatedAt time.Time `json:"createdAt" bson:"createdAt"` 37 | //更新时间 38 | UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` 39 | } 40 | 41 | type OauthToken struct { 42 | // Provider is the name of the OAuth2 provider[GitHub, Lark]. 43 | Provider string `uri:"provider" binding:"required"` 44 | // AccessToken is the token that authorizes and authenticates 45 | // the requests. 46 | AccessToken string `json:"accessToken"` 47 | 48 | // TokenType is the type of token. 49 | // The Type method returns either this or "Bearer", the default. 50 | TokenType string `json:"tokenType,omitempty"` 51 | 52 | // RefreshToken is a token that's used by the application 53 | // (as opposed to the user) to refresh the access token 54 | // if it expires. 55 | RefreshToken string `json:"refreshToken,omitempty"` 56 | 57 | // Expiry is the optional expiration time of the access token. 58 | // 59 | // If zero, TokenSource implementations will reuse the same 60 | // token forever and RefreshToken or equivalent 61 | // mechanisms for that TokenSource will not be used. 62 | Expiry *time.Time `json:"expiry,omitempty"` 63 | 64 | RefreshExpiry *time.Time `json:"refreshExpiry,omitempty"` 65 | } 66 | -------------------------------------------------------------------------------- /pkg/eks.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/pem" 7 | "fmt" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/service/eks" 11 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 12 | ) 13 | 14 | // FetchEKSKubeconfig fetches the kubeconfig for the specified EKS cluster. 15 | func FetchEKSKubeconfig(ctx context.Context, svc *eks.Client, region, clusterName string) (*clientcmdapi.Config, error) { 16 | result, err := svc.DescribeCluster(ctx, &eks.DescribeClusterInput{ 17 | Name: aws.String(clusterName), 18 | }) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | cluster := result.Cluster 24 | if cluster == nil { 25 | return nil, fmt.Errorf("cluster %s not found", clusterName) 26 | } 27 | 28 | // Decode and validate the Certificate Authority data 29 | certData, err := base64.StdEncoding.DecodeString(aws.ToString(cluster.CertificateAuthority.Data)) 30 | if err != nil { 31 | return nil, fmt.Errorf("failed to decode certificate authority data: %w", err) 32 | } 33 | 34 | pemBlock, _ := pem.Decode(certData) 35 | if pemBlock == nil { 36 | return nil, fmt.Errorf("failed to parse certificate authority data as PEM block") 37 | } 38 | 39 | return &clientcmdapi.Config{ 40 | APIVersion: "v1", 41 | Clusters: map[string]*clientcmdapi.Cluster{ 42 | clusterName: { 43 | Server: aws.ToString(cluster.Endpoint), 44 | CertificateAuthorityData: certData, 45 | }, 46 | }, 47 | Contexts: map[string]*clientcmdapi.Context{ 48 | "default": { 49 | Cluster: clusterName, 50 | AuthInfo: clusterName, 51 | }, 52 | }, 53 | CurrentContext: "default", 54 | AuthInfos: map[string]*clientcmdapi.AuthInfo{ 55 | clusterName: { 56 | Exec: &clientcmdapi.ExecConfig{ 57 | APIVersion: "client.authentication.k8s.io/v1beta1", 58 | Command: "aws", 59 | Args: []string{ 60 | "--region", 61 | region, 62 | "eks", 63 | "get-token", 64 | "--cluster-name", 65 | clusterName, 66 | }, 67 | InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode, 68 | ProvideClusterInfo: false, 69 | }, 70 | }, 71 | }, 72 | }, nil 73 | } 74 | -------------------------------------------------------------------------------- /service/monitor.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/shirou/gopsutil/v3/cpu" 8 | "github.com/shirou/gopsutil/v3/disk" 9 | "github.com/shirou/gopsutil/v3/mem" 10 | 11 | "github.com/mss-boot-io/mss-boot-admin/dto" 12 | ) 13 | 14 | /* 15 | * @Author: lwnmengjing 16 | * @Date: 2024/3/23 23:44:53 17 | * @Last Modified by: lwnmengjing 18 | * @Last Modified time: 2024/3/23 23:44:53 19 | */ 20 | 21 | type Monitor struct{} 22 | 23 | func (e *Monitor) Monitor(ctx *gin.Context) (*dto.MonitorResponse, error) { 24 | resp := &dto.MonitorResponse{ 25 | CPUInfo: make([]dto.MonitorCPUInfo, 0), 26 | } 27 | var err error 28 | resp.CPULogicalCore, err = cpu.CountsWithContext(ctx, true) 29 | if err != nil { 30 | return nil, err 31 | } 32 | resp.CPUPhysicalCore, err = cpu.CountsWithContext(ctx, false) 33 | if err != nil { 34 | return nil, err 35 | } 36 | cpuInfo, err := cpu.InfoWithContext(ctx) 37 | physicalCPU := make([]cpu.InfoStat, 0) 38 | for i := range cpuInfo { 39 | var exist bool 40 | for j := range physicalCPU { 41 | if cpuInfo[i].PhysicalID == physicalCPU[j].PhysicalID { 42 | exist = true 43 | break 44 | } 45 | } 46 | if !exist { 47 | physicalCPU = append(physicalCPU, cpuInfo[i]) 48 | } 49 | } 50 | // cpu使用率 51 | percent, err := cpu.PercentWithContext(ctx, time.Second, false) 52 | if err != nil { 53 | return nil, err 54 | } 55 | for i := range physicalCPU { 56 | resp.CPUInfo = append(resp.CPUInfo, dto.MonitorCPUInfo{ 57 | InfoStat: physicalCPU[i], 58 | CPUUsagePercent: percent[i] / 100, 59 | }) 60 | } 61 | // 内存 62 | m, err := mem.VirtualMemoryWithContext(ctx) 63 | if err != nil { 64 | return nil, err 65 | } 66 | resp.MemoryTotal = m.Total 67 | resp.MemoryUsage = m.Used 68 | resp.MemoryUsagePercent = m.UsedPercent / 100 69 | resp.MemoryAvailable = m.Available 70 | resp.MemoryFree = m.Free 71 | 72 | d, err := disk.Partitions(false) 73 | if err != nil { 74 | return nil, err 75 | } 76 | // 只计算总容量 77 | diskUsageStat, err := disk.Usage(d[0].Mountpoint) 78 | if err != nil { 79 | return nil, err 80 | } 81 | resp.DiskTotal = diskUsageStat.Total 82 | resp.DiskUsage = diskUsageStat.Used 83 | resp.DiskUsagePercent = diskUsageStat.UsedPercent 84 | 85 | return resp, nil 86 | } 87 | -------------------------------------------------------------------------------- /dto/user.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 7 | ) 8 | 9 | /* 10 | * @Author: lwnmengjing 11 | * @Date: 2023/8/6 22:16:32 12 | * @Last Modified by: lwnmengjing 13 | * @Last Modified time: 2023/8/6 22:16:32 14 | */ 15 | 16 | type RegisterRequest struct { 17 | Email string `json:"email" binding:"required,email"` 18 | Captcha string `json:"captcha" binding:"required"` 19 | Password string `json:"password" binding:"required"` 20 | } 21 | 22 | type RegisterResponse struct { 23 | } 24 | 25 | type ResetPasswordRequest struct { 26 | Email string `json:"email"` 27 | Captcha string `json:"captcha"` 28 | Password string `json:"password" binding:"required"` 29 | } 30 | 31 | type UserSearch struct { 32 | actions.Pagination `search:"inline"` 33 | // ID 34 | ID string `query:"id" form:"id" search:"type:contains;column:id"` 35 | //名称 36 | Name string `query:"name" form:"name" search:"type:contains;column:name"` 37 | } 38 | 39 | type LoginResponse struct { 40 | Code int `json:"code"` 41 | Expire time.Time `json:"expire"` 42 | Token string `json:"token"` 43 | } 44 | 45 | type FakeCaptchaRequest struct { 46 | Phone string `json:"phone"` 47 | Email string `json:"email"` 48 | UseBy string `json:"useBy"` 49 | } 50 | 51 | type FakeCaptchaResponse struct { 52 | Status string `json:"status"` 53 | } 54 | 55 | type PasswordResetRequest struct { 56 | UserID string `uri:"userID" binding:"required"` 57 | Password string `json:"password" binding:"required"` 58 | } 59 | 60 | type UpdateUserInfoRequest struct { 61 | // Name 昵称 62 | Name string `json:"name"` 63 | // Email 邮箱 64 | Email string `json:"email"` 65 | // Avatar 头像 66 | Avatar string `json:"avatar"` 67 | // Signature 个性签名 68 | Signature string `json:"signature"` 69 | // Title 职位 70 | Title string `json:"title"` 71 | // Group 组别 72 | Group string `json:"group"` 73 | // Country 国家 74 | Country string `json:"country"` 75 | // Province 省份 76 | Province string `json:"province"` 77 | // City 城市 78 | City string `json:"city"` 79 | // Address 地址 80 | Address string `json:"address"` 81 | // Phone 手机号 82 | Phone string `json:"phone"` 83 | // Profile 个人简介 84 | Profile string `json:"profile"` 85 | // Tags 标签 86 | Tags []string `json:"tags"` 87 | } 88 | 89 | type UpdateAvatarResponse struct { 90 | Avatar string `json:"avatar"` 91 | } 92 | -------------------------------------------------------------------------------- /config/application.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2023/8/6 08:11:42 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2023/8/6 08:11:42 8 | */ 9 | 10 | import ( 11 | "github.com/mss-boot-io/mss-boot/core/server" 12 | "github.com/mss-boot-io/mss-boot/core/server/listener" 13 | "github.com/mss-boot-io/mss-boot/pkg/config" 14 | "path/filepath" 15 | 16 | "github.com/gin-gonic/gin" 17 | ) 18 | 19 | type Mode string 20 | 21 | const ( 22 | ModeDev Mode = "dev" 23 | ModeTest Mode = "test" 24 | ModeProd Mode = "prod" 25 | ) 26 | 27 | type Application struct { 28 | Name string `yaml:"name" json:"name"` 29 | Mode Mode `yaml:"mode" json:"mode"` 30 | Origin string `yaml:"origin" json:"origin"` 31 | StaticPath map[string]string `yaml:"staticPath" json:"staticPath"` 32 | Labels map[string]string `yaml:"labels" json:"labels"` 33 | UI UIServer `yaml:"ui" json:"ui"` 34 | } 35 | 36 | func (e *Application) Init(r gin.IRouter) { 37 | if e.Mode == "" { 38 | e.Mode = ModeDev 39 | } 40 | 41 | switch e.Mode { 42 | case ModeDev: 43 | // set gin mode 44 | gin.SetMode(gin.DebugMode) 45 | 46 | // set static path 47 | for k := range e.StaticPath { 48 | //if k == "404" { 49 | // r.NoRoute(func(c *gin.Context) { 50 | // c.File(e.StaticPath[k]) 51 | // }) 52 | // continue 53 | //} 54 | if filepath.Ext(k) != "" { 55 | r.StaticFile(k, e.StaticPath[k]) 56 | continue 57 | } 58 | r.Static(k, e.StaticPath[k]) 59 | } 60 | r.StaticFile("/swagger.json", "docs/swagger.json") 61 | r.StaticFile("/swagger.yaml", "docs/swagger.yaml") 62 | case ModeTest: 63 | // set gin mode 64 | gin.SetMode(gin.TestMode) 65 | // no static 66 | case ModeProd: 67 | // set gin mode 68 | gin.SetMode(gin.ReleaseMode) 69 | // no static 70 | } 71 | } 72 | 73 | type UIServer struct { 74 | Enabled bool `yaml:"enabled" json:"enabled"` 75 | Path string `yaml:"path" json:"path"` 76 | config.Listen `yaml:",inline" json:",inline"` 77 | } 78 | 79 | func (u *UIServer) Init() server.Runnable { 80 | if !u.Enabled { 81 | return nil 82 | } 83 | 84 | r := gin.Default() 85 | r.Static("/", u.Path) 86 | r.LoadHTMLFiles(filepath.Join(u.Path, "index.html")) 87 | r.NoRoute(func(c *gin.Context) { 88 | c.HTML(200, "index.html", nil) 89 | }) 90 | return u.Listen.Init(listener.WithHandler(r)) 91 | } 92 | -------------------------------------------------------------------------------- /apis/github.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2022/10/19 15:28:20 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2022/10/19 15:28:20 8 | */ 9 | 10 | import ( 11 | "net/http" 12 | "strings" 13 | 14 | "github.com/mss-boot-io/mss-boot-admin/center" 15 | "golang.org/x/oauth2" 16 | 17 | "github.com/gin-gonic/gin" 18 | "github.com/mss-boot-io/mss-boot/pkg/response" 19 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 20 | 21 | "github.com/mss-boot-io/mss-boot-admin/dto" 22 | "github.com/mss-boot-io/mss-boot-admin/middleware" 23 | ) 24 | 25 | func init() { 26 | e := &Github{ 27 | Simple: controller.NewSimple(), 28 | } 29 | response.AppendController(e) 30 | } 31 | 32 | // Github github 33 | type Github struct { 34 | *controller.Simple 35 | } 36 | 37 | func (*Github) GetKey() string { 38 | return "github" 39 | } 40 | 41 | func (*Github) GetAction(string) response.Action { 42 | return nil 43 | } 44 | 45 | func (e *Github) Other(r *gin.RouterGroup) { 46 | r.Use(middleware.GetMiddlewares()...) 47 | r.GET("/github/get-login-url", e.GetLoginURL) 48 | } 49 | 50 | // GetLoginURL 获取github登录地址 51 | // @Summary 获取github登录地址 52 | // @Description 获取github登录地址 53 | // @Tags generator 54 | // @Accept application/json 55 | // @Product application/json 56 | // @Param state query string true "state" 57 | // @Success 200 {object} string 58 | // @Router /admin/api/github/get-login-url [get] 59 | func (e *Github) GetLoginURL(c *gin.Context) { 60 | api := response.Make(c) 61 | req := &dto.OauthGetLoginURLReq{} 62 | if api.Bind(req).Error != nil { 63 | api.Err(http.StatusUnprocessableEntity) 64 | return 65 | } 66 | clientID, _ := center.GetAppConfig().GetAppConfig(c, "security:githubClientId") 67 | clientSecret, _ := center.GetAppConfig().GetAppConfig(c, "security:githubClientSecret") 68 | redirectURL, _ := center.GetAppConfig().GetAppConfig(c, "security:githubRedirectURL") 69 | scopes, _ := center.GetAppConfig().GetAppConfig(c, "security:githubScope") 70 | conf := &oauth2.Config{ 71 | ClientID: clientID, 72 | ClientSecret: clientSecret, 73 | Scopes: strings.Split(scopes, ","), 74 | RedirectURL: redirectURL, 75 | Endpoint: oauth2.Endpoint{ 76 | AuthURL: "https://github.com/login/oauth/authorize", 77 | TokenURL: "https://github.com/login/oauth/access_token", 78 | }, 79 | } 80 | //api.OK(conf.AuthCodeURL(req.State)) 81 | c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(conf.AuthCodeURL(req.State))) 82 | } 83 | -------------------------------------------------------------------------------- /service/storage.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "mime/multipart" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/service/s3" 12 | "github.com/gin-gonic/gin" 13 | "github.com/mss-boot-io/mss-boot/pkg/config" 14 | 15 | "github.com/mss-boot-io/mss-boot-admin/center" 16 | ) 17 | 18 | /* 19 | * @Author: lwnmengjing 20 | * @Date: 2024/3/29 00:42:54 21 | * @Last Modified by: lwnmengjing 22 | * @Last Modified time: 2024/3/29 00:42:54 23 | */ 24 | 25 | type Storage struct{} 26 | 27 | func (s *Storage) Upload(c *gin.Context, f *multipart.FileHeader, tenantID, userID string) (string, error) { 28 | storageType, _ := center.GetAppConfig().GetAppConfig(c, "storage:type") 29 | endpoint, _ := center.GetAppConfig().GetAppConfig(c, "storage:endpoint") 30 | switch storageType { 31 | case "s3": 32 | storage := config.Storage{} 33 | s3Type, _ := center.GetAppConfig().GetAppConfig(c, "storage:type") 34 | if s3Type == "" { 35 | s3Type = string(config.S3) 36 | } 37 | storage.Type = config.ProviderType(s3Type) 38 | storage.Region, _ = center.GetAppConfig().GetAppConfig(c, "storage:s3Region") 39 | storage.Endpoint, _ = center.GetAppConfig().GetAppConfig(c, "storage:s3Endpoint") 40 | storage.Bucket, _ = center.GetAppConfig().GetAppConfig(c, "storage:s3Bucket") 41 | storage.AccessKeyID, _ = center.GetAppConfig().GetAppConfig(c, "storage:s3AccessKeyID") 42 | storage.SecretAccessKey, _ = center.GetAppConfig().GetAppConfig(c, "storage:s3SecretAccessKey") 43 | storage.SigningMethod, _ = center.GetAppConfig().GetAppConfig(c, "storage:s3SigningMethod") 44 | storage.Init() 45 | //上传文件对象存储 46 | file, err := f.Open() 47 | if err != nil { 48 | return "", err 49 | } 50 | key := fmt.Sprintf("%s/%s/%s", tenantID, userID, f.Filename) 51 | _, err = storage.GetClient().PutObject(c, &s3.PutObjectInput{ 52 | Bucket: &storage.Bucket, 53 | Key: aws.String(key), 54 | Body: file, 55 | }) 56 | if err != nil { 57 | return "", err 58 | } 59 | return fmt.Sprintf("%s/%s", endpoint, key), nil 60 | default: 61 | //默认local 62 | if endpoint == "" { 63 | return "", errors.New("localEndpoint is empty") 64 | } 65 | //上传文件到本地 66 | key := filepath.Join("public", tenantID, userID, f.Filename) 67 | err := c.SaveUploadedFile(f, key) 68 | if err != nil { 69 | return "", err 70 | } 71 | return strings.Join([]string{endpoint, "public", tenantID, userID, f.Filename}, "/"), nil 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /compose/redis/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | networks: 4 | redis-replication: 5 | driver: bridge 6 | ipam: 7 | config: 8 | - subnet: 172.25.0.0/24 9 | 10 | services: 11 | master: 12 | image: redis 13 | container_name: redis-master 14 | ports: 15 | - "6380:6379" 16 | volumes: 17 | - "./master/redis.conf:/etc/redis.conf" 18 | - "./master/data:/data" 19 | command: ["redis-server", "/etc/redis.conf"] 20 | restart: always 21 | networks: 22 | redis-replication: 23 | ipv4_address: 172.25.0.101 24 | 25 | slave1: 26 | image: redis 27 | container_name: redis-slave-1 28 | ports: 29 | - "6381:6379" 30 | volumes: 31 | - "./slave1/redis.conf:/etc/redis.conf" 32 | - "./slave1/data:/data" 33 | command: ["redis-server", "/etc/redis.conf"] 34 | restart: always 35 | networks: 36 | redis-replication: 37 | ipv4_address: 172.25.0.102 38 | 39 | slave2: 40 | image: redis 41 | container_name: redis-slave-2 42 | ports: 43 | - "6382:6379" 44 | volumes: 45 | - "./slave2/redis.conf:/etc/redis.conf" 46 | - "./slave2/data:/data" 47 | command: ["redis-server", "/etc/redis.conf"] 48 | restart: always 49 | networks: 50 | redis-replication: 51 | ipv4_address: 172.25.0.103 52 | 53 | sentinel1: 54 | image: redis 55 | container_name: redis-sentinel-1 56 | ports: 57 | - "26380:26379" 58 | volumes: 59 | - "./sentinel1/sentinel.conf:/etc/sentinel.conf" 60 | command: ["/bin/bash", "-c", "cp /etc/sentinel.conf /sentinel.conf && redis-sentinel /sentinel.conf"] 61 | restart: always 62 | networks: 63 | redis-replication: 64 | ipv4_address: 172.25.0.201 65 | 66 | sentinel2: 67 | image: redis 68 | container_name: redis-sentinel-2 69 | ports: 70 | - "26381:26379" 71 | volumes: 72 | - "./sentinel2/sentinel.conf:/etc/sentinel.conf" 73 | command: ["/bin/bash", "-c", "cp /etc/sentinel.conf /sentinel.conf && redis-sentinel /sentinel.conf"] 74 | restart: always 75 | networks: 76 | redis-replication: 77 | ipv4_address: 172.25.0.202 78 | 79 | sentinel3: 80 | image: redis 81 | container_name: redis-sentinel-3 82 | ports: 83 | - "26382:26379" 84 | volumes: 85 | - "./sentinel3/sentinel.conf:/etc/sentinel.conf" 86 | command: ["/bin/bash", "-c", "cp /etc/sentinel.conf /sentinel.conf && redis-sentinel /sentinel.conf"] 87 | restart: always 88 | networks: 89 | redis-replication: 90 | ipv4_address: 172.25.0.203 91 | -------------------------------------------------------------------------------- /models/user_config.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "gorm.io/gorm/clause" 6 | "strings" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | 11 | "github.com/mss-boot-io/mss-boot-admin/center" 12 | ) 13 | 14 | /* 15 | * @Author: lwnmengjing 16 | * @Date: 2024/3/2 00:21:01 17 | * @Last Modified by: lwnmengjing 18 | * @Last Modified time: 2024/3/2 00:21:01 19 | */ 20 | 21 | type UserConfig struct { 22 | ModelGormTenant 23 | // UserID 用户id 24 | UserID string `json:"userID" gorm:"size:64;index;default:'';not null;comment:用户id" binding:"required"` 25 | // Name 名称 26 | Name string `json:"name" gorm:"size:128;index;default:'';not null;comment:名称" binding:"required"` 27 | // Group 分组 28 | Group string `json:"group" gorm:"size:128;default:'';not null;comment:分组" binding:"required"` 29 | // Value 值 30 | Value string `json:"value" gorm:"size:255;default:'';not null;comment:值"` 31 | } 32 | 33 | func (*UserConfig) TableName() string { 34 | return "mss_boot_user_configs" 35 | } 36 | 37 | func (e *UserConfig) SetUserConfig(ctx *gin.Context, userID, key string, value string) error { 38 | if key == "" { 39 | return nil 40 | } 41 | 42 | var group string 43 | keys := strings.Split(key, ".") 44 | if len(keys) > 1 { 45 | group = keys[0] 46 | key = strings.Join(keys[1:], ".") 47 | } 48 | c := &UserConfig{ 49 | Group: group, 50 | Name: key, 51 | UserID: userID, 52 | Value: value, 53 | } 54 | c.UpdatedAt = time.Now() 55 | return center.GetTenant().GetDB(ctx, e). 56 | Clauses(clause.OnConflict{ 57 | Columns: []clause.Column{ 58 | {Name: "tenant_id"}, 59 | {Name: "user_id"}, 60 | {Name: "name"}, 61 | {Name: "group"}, 62 | }, 63 | DoUpdates: clause.AssignmentColumns([]string{"value", "updated_at"}), 64 | }). 65 | Create(c).Error 66 | } 67 | 68 | func getUserConfig(ctx *gin.Context, userID, key string) (*UserConfig, error) { 69 | c := &UserConfig{} 70 | if key == "" { 71 | return nil, fmt.Errorf("key is empty") 72 | } 73 | 74 | var group string 75 | keys := strings.Split(key, ".") 76 | if len(keys) > 1 { 77 | group = keys[0] 78 | key = strings.Join(keys[1:], ".") 79 | } 80 | err := center.GetTenant().GetDB(ctx, c). 81 | Where("group = ?", group). 82 | Where("user_id = ?", userID). 83 | Where("name = ?", key). 84 | First(c).Error 85 | if err != nil { 86 | return nil, err 87 | } 88 | return c, nil 89 | } 90 | 91 | func (e *UserConfig) GetUserConfig(ctx *gin.Context, userID, key string) (string, bool) { 92 | c, err := getUserConfig(ctx, userID, key) 93 | if err != nil { 94 | return "", false 95 | } 96 | return c.Value, true 97 | } 98 | -------------------------------------------------------------------------------- /models/user_oauth2.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/mss-boot-io/mss-boot-admin/pkg" 4 | 5 | /* 6 | * @Author: lwnmengjing 7 | * @Date: 2023/12/11 13:48:02 8 | * @Last Modified by: lwnmengjing 9 | * @Last Modified time: 2023/12/11 13:48:02 10 | */ 11 | 12 | type UserOAuth2 struct { 13 | ModelGormTenant 14 | User *User `json:"user" gorm:"foreignKey:UserID;references:ID" swaggerignore:"true"` 15 | UserID string `json:"user_id" gorm:"size:64"` 16 | OpenID string `json:"openID" gorm:"size:64"` 17 | UnionID string `json:"unionID" gorm:"column:union_id;size:64"` 18 | Sub string `json:"sub" gorm:"size:255;comment:主题"` 19 | Name string `json:"name" gorm:"size:255;comment:名称"` 20 | GivenName string `json:"given_name" gorm:"size:255;comment:名"` 21 | FamilyName string `json:"family_name" gorm:"size:255;comment:姓"` 22 | MiddleName string `json:"middle_name" gorm:"size:255;comment:中间名"` 23 | NickName string `json:"nickname" gorm:"size:255;comment:昵称"` 24 | PreferredUsername string `json:"preferred_username" gorm:"size:255;comment:首选用户名"` 25 | Profile string `json:"profile" gorm:"size:255;comment:个人资料"` 26 | Picture string `json:"picture" gorm:"size:255;comment:图片"` 27 | Website string `json:"website" gorm:"size:255;comment:网站"` 28 | Email string `json:"email" gorm:"size:255;comment:邮箱"` 29 | EmailVerified bool `json:"email_verified" gorm:"default:false;comment:邮箱是否验证"` 30 | Gender string `json:"gender" gorm:"size:255;comment:性别"` 31 | Birthdata string `json:"birthdata" gorm:"size:255;comment:出生日期"` 32 | Zoneinfo string `json:"zoneinfo" gorm:"size:255;comment:时区"` 33 | Locale string `json:"locale" gorm:"size:255;comment:语言"` 34 | PhoneNumber string `json:"phone_number" gorm:"size:255;comment:手机号"` 35 | PhoneNumberVerified bool `json:"phone_number_verified" gorm:"default:false;comment:手机号是否验证"` 36 | Address string `json:"address" gorm:"size:255;comment:地址"` 37 | EmployeeNO string `json:"employee_no" gorm:"column:employee_no;size:255;comment:员工编号"` 38 | Provider pkg.LoginProvider `json:"type" gorm:"size:20;comment:登录类型"` 39 | } 40 | 41 | func (*UserOAuth2) TableName() string { 42 | return "mss_boot_user_oauth2" 43 | } 44 | -------------------------------------------------------------------------------- /apis/api.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/mss-boot-io/mss-boot-admin/dto" 6 | "github.com/mss-boot-io/mss-boot-admin/models" 7 | "github.com/mss-boot-io/mss-boot/pkg/response" 8 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 9 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 10 | ) 11 | 12 | /* 13 | * @Author: lwnmengjing 14 | * @Date: 2023/8/24 01:47:02 15 | * @Last Modified by: lwnmengjing 16 | * @Last Modified time: 2023/8/24 01:47:02 17 | */ 18 | 19 | func init() { 20 | e := &API{ 21 | Simple: controller.NewSimple( 22 | controller.WithAuth(true), 23 | controller.WithModel(new(models.API)), 24 | controller.WithSearch(new(dto.APISearch)), 25 | controller.WithModelProvider(actions.ModelProviderGorm), 26 | ), 27 | } 28 | response.AppendController(e) 29 | } 30 | 31 | type API struct { 32 | *controller.Simple 33 | } 34 | 35 | // Create 创建API 36 | // @Summary 创建API 37 | // @Description 创建API 38 | // @Tags api 39 | // @Accept application/json 40 | // @Accept application/json 41 | // @Param data body models.API true "data" 42 | // @Success 201 {object} models.API 43 | // @Router /admin/api/apis [post] 44 | // @Security Bearer 45 | func (e *API) Create(*gin.Context) {} 46 | 47 | // Update 更新API 48 | // @Summary 更新API 49 | // @Description 更新API 50 | // @Tags api 51 | // @Accept application/json 52 | // @Accept application/json 53 | // @Param id path string true "id" 54 | // @Param data body models.API true "data" 55 | // @Success 200 {object} models.API 56 | // @Router /admin/api/apis/{id} [put] 57 | // @Security Bearer 58 | func (e *API) Update(*gin.Context) {} 59 | 60 | // Delete 删除API 61 | // @Summary 删除API 62 | // @Description 删除API 63 | // @Tags api 64 | // @Accept application/json 65 | // @Param id path string true "id" 66 | // @Success 204 67 | // @Router /admin/api/apis/{id} [delete] 68 | // @Security Bearer 69 | func (e *API) Delete(*gin.Context) {} 70 | 71 | // Get 获取API 72 | // @Summary 获取API 73 | // @Description 获取API 74 | // @Tags api 75 | // @Accept application/json 76 | // @Param id path string true "id" 77 | // @Success 200 {object} models.API 78 | // @Router /admin/api/apis/{id} [get] 79 | // @Security Bearer 80 | func (e *API) Get(*gin.Context) {} 81 | 82 | // List API列表数据 83 | // @Summary API列表数据 84 | // @Description API列表数据 85 | // @Tags api 86 | // @Accept application/json 87 | // @Accept application/json 88 | // @Param current query int false "current" 89 | // @Param pageSize query int false "pageSize" 90 | // @Success 200 {object} response.Page{data=[]models.API} 91 | // @Router /admin/api/apis [get] 92 | // @Security Bearer 93 | func (e *API) List(*gin.Context) {} 94 | -------------------------------------------------------------------------------- /config/option_redis.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2024/3/1 10:14:14 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2024/3/1 10:14:14 8 | */ 9 | 10 | import ( 11 | "context" 12 | "crypto/tls" 13 | "crypto/x509" 14 | "fmt" 15 | "os" 16 | 17 | "github.com/redis/go-redis/v9" 18 | ) 19 | 20 | var _redis *redis.Client 21 | 22 | // GetRedisClient 获取redis客户端 23 | func GetRedisClient() *redis.Client { 24 | return _redis 25 | } 26 | 27 | // SetRedisClient 设置redis客户端 28 | func SetRedisClient(c *redis.Client) { 29 | if _redis != nil && _redis != c { 30 | _redis.Shutdown(context.TODO()) 31 | } 32 | _redis = c 33 | } 34 | 35 | type RedisConnectOptions struct { 36 | Network string `yaml:"network" json:"network"` 37 | Addr string `yaml:"addr" json:"addr"` 38 | Username string `yaml:"username" json:"username"` 39 | Password string `yaml:"password" json:"password"` 40 | DB int `yaml:"db" json:"db"` 41 | PoolSize int `yaml:"pool_size" json:"pool_size"` 42 | Tls *TLS `yaml:"tls" json:"tls"` 43 | MaxRetries int `yaml:"max_retries" json:"max_retries"` 44 | } 45 | 46 | type TLS struct { 47 | Cert string `yaml:"cert" json:"cert"` 48 | Key string `yaml:"key" json:"key"` 49 | Ca string `yaml:"ca" json:"ca"` 50 | } 51 | 52 | func (e RedisConnectOptions) GetRedisOptions() (*redis.Options, error) { 53 | r := &redis.Options{ 54 | Network: e.Network, 55 | Addr: e.Addr, 56 | Username: e.Username, 57 | Password: e.Password, 58 | DB: e.DB, 59 | MaxRetries: e.MaxRetries, 60 | PoolSize: e.PoolSize, 61 | } 62 | var err error 63 | r.TLSConfig, err = getTLS(e.Tls) 64 | return r, err 65 | } 66 | 67 | func getTLS(c *TLS) (*tls.Config, error) { 68 | if c != nil && c.Cert != "" { 69 | // 从证书相关文件中读取和解析信息,得到证书公钥、密钥对 70 | cert, err := tls.LoadX509KeyPair(c.Cert, c.Key) 71 | if err != nil { 72 | fmt.Printf("tls.LoadX509KeyPair err: %v\n", err) 73 | return nil, err 74 | } 75 | // 创建一个新的、空的 CertPool,并尝试解析 PEM 编码的证书,解析成功会将其加到 CertPool 中 76 | certPool := x509.NewCertPool() 77 | 78 | ca, err := os.ReadFile(c.Ca) 79 | if err != nil { 80 | fmt.Printf("os.ReadFile err: %v\n", err) 81 | return nil, err 82 | } 83 | 84 | if ok := certPool.AppendCertsFromPEM(ca); !ok { 85 | fmt.Println("certPool.AppendCertsFromPEM err") 86 | return nil, err 87 | } 88 | return &tls.Config{ 89 | // 设置证书链,允许包含一个或多个 90 | Certificates: []tls.Certificate{cert}, 91 | // 要求必须校验客户端的证书 92 | ClientAuth: tls.RequireAndVerifyClientCert, 93 | // 设置根证书的集合,校验方式使用 ClientAuth 中设定的模式 94 | ClientCAs: certPool, 95 | }, nil 96 | } 97 | return nil, nil 98 | } 99 | -------------------------------------------------------------------------------- /apis/field.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/mss-boot-io/mss-boot/pkg/response" 6 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 7 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 8 | 9 | "github.com/mss-boot-io/mss-boot-admin/dto" 10 | "github.com/mss-boot-io/mss-boot-admin/models" 11 | ) 12 | 13 | /* 14 | * @Author: lwnmengjing 15 | * @Date: 2023/12/29 21:55:50 16 | * @Last Modified by: lwnmengjing 17 | * @Last Modified time: 2023/12/29 21:55:50 18 | */ 19 | 20 | func init() { 21 | e := &Field{ 22 | Simple: controller.NewSimple( 23 | controller.WithAuth(true), 24 | controller.WithModel(new(models.Field)), 25 | controller.WithSearch(new(dto.FieldSearch)), 26 | controller.WithModelProvider(actions.ModelProviderGorm), 27 | ), 28 | } 29 | response.AppendController(e) 30 | } 31 | 32 | type Field struct { 33 | *controller.Simple 34 | } 35 | 36 | // Create 创建字段 37 | // @Summary 创建字段 38 | // @Description 创建字段 39 | // @Tags field 40 | // @Accept application/json 41 | // @Product application/json 42 | // @Param data body models.Field true "data" 43 | // @Success 201 {object} models.Field 44 | // @Router /admin/api/fields [post] 45 | // @Security Bearer 46 | func (e *Field) Create(*gin.Context) {} 47 | 48 | // Update 更新字段 49 | // @Summary 更新字段 50 | // @Description 更新字段 51 | // @Tags field 52 | // @Accept application/json 53 | // @Product application/json 54 | // @Param id path string true "id" 55 | // @Param data body models.Field true "data" 56 | // @Success 200 {object} models.Field 57 | // @Router /admin/api/fields/{id} [put] 58 | // @Security Bearer 59 | func (e *Field) Update(*gin.Context) {} 60 | 61 | // Delete 删除字段 62 | // @Summary 删除字段 63 | // @Description 删除字段 64 | // @Tags field 65 | // @Param id path string true "id" 66 | // @Success 204 67 | // @Router /admin/api/fields/{id} [delete] 68 | // @Security Bearer 69 | func (e *Field) Delete(*gin.Context) {} 70 | 71 | // Get 获取字段 72 | // @Summary 获取字段 73 | // @Description 获取字段 74 | // @Tags field 75 | // @Param id path string true "id" 76 | // @Success 200 {object} models.Field 77 | // @Router /admin/api/fields/{id} [get] 78 | // @Security Bearer 79 | func (e *Field) Get(*gin.Context) {} 80 | 81 | // List 字段列表 82 | // @Summary 字段列表 83 | // @Description 字段列表 84 | // @Tags field 85 | // @Accept application/json 86 | // @Product application/json 87 | // @Param current query int false "current" 88 | // @Param pageSize query int false "pageSize" 89 | // @Param modelID query string false "modelID" 90 | // @Success 200 {object} response.Page{data=[]models.Field} 91 | // @Router /admin/api/fields [get] 92 | // @Security Bearer 93 | func (e *Field) List(*gin.Context) {} 94 | -------------------------------------------------------------------------------- /pkg/pack/zip_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lwnmengjing 3 | * @Date: 2022/10/28 03:32:35 4 | * @Last Modified by: lwnmengjing 5 | * @Last Modified time: 2022/10/28 03:32:35 6 | */ 7 | 8 | package pack 9 | 10 | import ( 11 | "os" 12 | "testing" 13 | ) 14 | 15 | func TestZip(t *testing.T) { 16 | type args struct { 17 | root string 18 | src []string 19 | ignore []string 20 | file string 21 | } 22 | tests := []struct { 23 | name string 24 | args args 25 | wantErr bool 26 | }{ 27 | { 28 | name: "test zip", 29 | args: args{ 30 | root: "../../testdata", 31 | ignore: []string{"test.tar.gz", "test.zip"}, 32 | file: "../../testdata/test.zip", 33 | }, 34 | wantErr: false, 35 | }, 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | writer, err := os.Create(tt.args.file) 40 | if (err != nil) != tt.wantErr { 41 | t.Errorf("Zip() error = %v, wantErr %v", err, tt.wantErr) 42 | return 43 | } 44 | defer writer.Close() 45 | err = Zip(tt.args.root, tt.args.src, writer, tt.args.ignore...) 46 | if (err != nil) != tt.wantErr { 47 | t.Errorf("Zip() error = %v, wantErr %v", err, tt.wantErr) 48 | return 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func TestUnzip(t *testing.T) { 55 | type args struct { 56 | src string 57 | dst string 58 | } 59 | tests := []struct { 60 | name string 61 | args args 62 | wantErr bool 63 | }{ 64 | { 65 | name: "test unzip", 66 | args: args{ 67 | src: "../../testdata/test.zip", 68 | dst: "../../testdata/test", 69 | }, 70 | wantErr: false, 71 | }, 72 | } 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | err := Unzip(tt.args.src, tt.args.dst) 76 | if (err != nil) != tt.wantErr { 77 | t.Errorf("Zip() error = %v, wantErr %v", err, tt.wantErr) 78 | return 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestUnzipByContent(t *testing.T) { 85 | content, _ := os.ReadFile("../../testdata/test.zip") 86 | type args struct { 87 | content []byte 88 | dst string 89 | } 90 | tests := []struct { 91 | name string 92 | args args 93 | wantErr bool 94 | }{ 95 | { 96 | name: "test unzip", 97 | args: args{ 98 | content: content, 99 | dst: "../../testdata/test", 100 | }, 101 | wantErr: false, 102 | }, 103 | } 104 | for _, tt := range tests { 105 | t.Run(tt.name, func(t *testing.T) { 106 | err := UnzipByContent(tt.args.content, tt.args.dst) 107 | if (err != nil) != tt.wantErr { 108 | t.Errorf("Zip() error = %v, wantErr %v", err, tt.wantErr) 109 | return 110 | } 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /pkg/pack/tar_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lwnmengjing 3 | * @Date: 2022/10/28 03:38:06 4 | * @Last Modified by: lwnmengjing 5 | * @Last Modified time: 2022/10/28 03:38:06 6 | */ 7 | 8 | package pack 9 | 10 | import ( 11 | "os" 12 | "testing" 13 | ) 14 | 15 | func TestTar(t *testing.T) { 16 | type args struct { 17 | root string 18 | src []string 19 | ignore []string 20 | file string 21 | } 22 | tests := []struct { 23 | name string 24 | args args 25 | wantErr bool 26 | }{ 27 | { 28 | 29 | name: "test tar", 30 | args: args{ 31 | root: "../../testdata", 32 | ignore: []string{"test.tar.gz", "test.zip"}, 33 | file: "../../testdata/test.tar.gz", 34 | }, 35 | wantErr: false, 36 | }, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | writer, err := os.Create(tt.args.file) 41 | if (err != nil) != tt.wantErr { 42 | t.Errorf("Zip() error = %v, wantErr %v", err, tt.wantErr) 43 | return 44 | } 45 | defer writer.Close() 46 | err = Tar(tt.args.root, tt.args.src, writer, tt.args.ignore...) 47 | if (err != nil) != tt.wantErr { 48 | t.Errorf("Tar() error = %v, wantErr %v", err, tt.wantErr) 49 | return 50 | } 51 | }) 52 | } 53 | } 54 | 55 | func TestTarX(t *testing.T) { 56 | type args struct { 57 | src string 58 | dst string 59 | } 60 | tests := []struct { 61 | name string 62 | args args 63 | wantErr bool 64 | }{ 65 | { 66 | name: "test unzip", 67 | args: args{ 68 | src: "../../testdata/test.tar.gz", 69 | dst: "../../testdata/test", 70 | }, 71 | wantErr: false, 72 | }, 73 | } 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | err := TarX(tt.args.src, tt.args.dst) 77 | if (err != nil) != tt.wantErr { 78 | t.Errorf("Zip() error = %v, wantErr %v", err, tt.wantErr) 79 | return 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestTarXByContent(t *testing.T) { 86 | content, _ := os.ReadFile("../../testdata/test.tar.gz") 87 | type args struct { 88 | content []byte 89 | dst string 90 | } 91 | tests := []struct { 92 | name string 93 | args args 94 | wantErr bool 95 | }{ 96 | { 97 | name: "test unzip", 98 | args: args{ 99 | content: content, 100 | dst: "../../testdata/test", 101 | }, 102 | wantErr: false, 103 | }, 104 | } 105 | for _, tt := range tests { 106 | t.Run(tt.name, func(t *testing.T) { 107 | err := TarXByContent(tt.args.content, tt.args.dst) 108 | if (err != nil) != tt.wantErr { 109 | t.Errorf("Zip() error = %v, wantErr %v", err, tt.wantErr) 110 | return 111 | } 112 | }) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /models/department.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/mss-boot-io/mss-boot-admin/pkg" 7 | "github.com/mss-boot-io/mss-boot/pkg/enum" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | /* 12 | * @Author: lwnmengjing 13 | * @Date: 2024/1/24 18:11:32 14 | * @Last Modified by: lwnmengjing 15 | * @Last Modified time: 2024/1/24 18:11:32 16 | */ 17 | 18 | type DepartmentList []*Department 19 | 20 | type Department struct { 21 | ModelGormTenant 22 | // ParentID 父级id 23 | ParentID string `json:"parentID,omitempty" gorm:"column:parent_id;comment:父级id;type:varchar(255);default:'';index"` 24 | // Name 部门名称 25 | Name string `json:"name" gorm:"column:name;comment:部门名称;type:varchar(255);not null"` 26 | // LeaderID 部分负责人ID 27 | LeaderID string `json:"leaderID" gorm:"column:leader_id;comment:部分负责人id;type:varchar(64)"` 28 | // Phone 联系电话 29 | Phone string `json:"phone" gorm:"column:phone;comment:联系电话;type:varchar(255)"` 30 | // Email 邮箱 31 | Email string `json:"email" gorm:"column:email;comment:邮箱;type:varchar(255)"` 32 | // Code 部门编码 33 | Code string `json:"code" gorm:"column:code;comment:部门编码;type:varchar(255);not null"` 34 | // Status 状态 35 | Status enum.Status `json:"status" gorm:"column:status;comment:状态;size:10"` 36 | // Sort 排序 37 | Sort int `json:"sort" gorm:"column:sort;comment:排序;size:5;defualt:0"` 38 | // Children 子部门 39 | Children []*Department `json:"children,omitempty" gorm:"foreignKey:ParentID;references:ID" swaggerignore:"true"` 40 | } 41 | 42 | func (*Department) TableName() string { 43 | return "mss_boot_departments" 44 | } 45 | 46 | func (x DepartmentList) Len() int { return len(x) } 47 | func (x DepartmentList) Less(i, j int) bool { return x[i].Sort > x[j].Sort } 48 | func (x DepartmentList) Swap(i, j int) { x[i], x[j] = x[j], x[i] } 49 | 50 | func (e *Department) GetAllChildrenID(tx *gorm.DB) []string { 51 | ids := []string{e.ID} 52 | if len(e.Children) == 0 { 53 | tx.Model(&Department{}).Where("parent_id = ?", e.ID).Find(&e.Children) 54 | } 55 | for i := range e.Children { 56 | ids = append(ids, e.Children[i].GetAllChildrenID(tx)...) 57 | } 58 | return ids 59 | } 60 | 61 | func (e *Department) GetIndex() string { 62 | return e.ID 63 | } 64 | 65 | func (e *Department) GetParentID() string { 66 | return e.ParentID 67 | } 68 | 69 | func (e *Department) AddChildren(children []pkg.TreeImp) { 70 | if e.Children == nil { 71 | e.Children = make([]*Department, 0) 72 | } 73 | for i := range children { 74 | e.Children = append(e.Children, children[i].(*Department)) 75 | } 76 | } 77 | 78 | func (e *Department) SortChildren() { 79 | if len(e.Children) == 0 { 80 | return 81 | } 82 | sort.Sort(DepartmentList(e.Children)) 83 | for i := range e.Children { 84 | e.Children[i].SortChildren() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2024/1/9 17:59:55 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2024/1/9 17:59:55 8 | */ 9 | 10 | import ( 11 | "net/http" 12 | 13 | "github.com/gin-contrib/cors" 14 | "github.com/gin-gonic/gin" 15 | _ "github.com/mss-boot-io/mss-boot-admin/apis" 16 | "github.com/mss-boot-io/mss-boot-admin/config" 17 | "github.com/mss-boot-io/mss-boot/pkg/response" 18 | swaggerFiles "github.com/swaggo/files" 19 | ginSwagger "github.com/swaggo/gin-swagger" 20 | ) 21 | 22 | func InitRouter(r *gin.RouterGroup) { 23 | v1 := r.Group("/api") 24 | if config.Cfg.Application.Mode == config.ModeDev { 25 | r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 26 | } 27 | configCors := cors.DefaultConfig() 28 | configCors.AllowOrigins = []string{"*"} 29 | configCors.AllowCredentials = true 30 | configCors.AddAllowHeaders("Authorization") 31 | v1.Use(cors.New(configCors)) 32 | v1.OPTIONS("/*path", func(c *gin.Context) { 33 | c.Status(http.StatusNoContent) 34 | }) 35 | 36 | for i := range response.Controllers { 37 | response.Controllers[i].Other(r.Group("/api", cors.New(configCors))) 38 | e := v1.Group(response.Controllers[i].Path(), response.Controllers[i].Handlers()...) 39 | if action := response.Controllers[i].GetAction(response.Get); action != nil { 40 | e.GET("/:"+response.Controllers[i].GetKey(), action.Handler()...) 41 | } 42 | if action := response.Controllers[i].GetAction(response.Control); action != nil { 43 | e.POST("", action.Handler()...) 44 | e.PUT("/:"+response.Controllers[i].GetKey(), action.Handler()...) 45 | } 46 | if action := response.Controllers[i].GetAction(response.Create); action != nil { 47 | e.POST("", action.Handler()...) 48 | } 49 | if action := response.Controllers[i].GetAction(response.Update); action != nil { 50 | e.PUT("/:"+response.Controllers[i].GetKey(), action.Handler()...) 51 | } 52 | if action := response.Controllers[i].GetAction(response.Delete); action != nil { 53 | e.DELETE("/:"+response.Controllers[i].GetKey(), action.Handler()...) 54 | } 55 | if action := response.Controllers[i].GetAction(response.Search); action != nil { 56 | e.GET("", action.Handler()...) 57 | } 58 | } 59 | } 60 | 61 | var DefaultMakeRouter = &MakeRouter{ 62 | funcs: []func(*gin.RouterGroup){InitRouter}, 63 | } 64 | 65 | type MakeRouter struct { 66 | funcs []func(*gin.RouterGroup) 67 | } 68 | 69 | func (m *MakeRouter) SetFunc(f ...func(*gin.RouterGroup)) { 70 | if m.funcs == nil { 71 | m.funcs = make([]func(*gin.RouterGroup), 0) 72 | } 73 | m.funcs = append(m.funcs, f...) 74 | } 75 | 76 | func (m *MakeRouter) GetFunc() []func(*gin.RouterGroup) { 77 | return m.funcs 78 | } 79 | 80 | func (m *MakeRouter) MakeRouter(r *gin.RouterGroup) { 81 | for i := range m.funcs { 82 | m.funcs[i](r) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /models/type.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/mss-boot-io/mss-boot-admin/center" 11 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 12 | "github.com/spf13/cast" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | /* 17 | * @Author: lwnmengjing 18 | * @Date: 2023/12/5 23:09:49 19 | * @Last Modified by: lwnmengjing 20 | * @Last Modified time: 2023/12/5 23:09:49 21 | */ 22 | 23 | type JsonRawMessage string 24 | 25 | func (j *JsonRawMessage) Scan(val any) error { 26 | if val == nil { 27 | return nil 28 | } 29 | s := cast.ToString(val) 30 | *j = JsonRawMessage(s) 31 | return nil 32 | } 33 | 34 | func (j *JsonRawMessage) Value() (driver.Value, error) { 35 | if len(*j) == 0 { 36 | return nil, nil 37 | } 38 | return json.RawMessage(*j), nil 39 | } 40 | 41 | type ArrayString []string 42 | 43 | func (a *ArrayString) Scan(val any) error { 44 | var s string 45 | switch val.(type) { 46 | case []uint8: 47 | // support mysql 48 | s = string(val.([]uint8)) 49 | case string: 50 | // support sqlite 51 | s = val.(string) 52 | } 53 | ss := strings.Split(s, "|") 54 | *a = ss 55 | return nil 56 | } 57 | 58 | func (a *ArrayString) Value() (driver.Value, error) { 59 | return strings.Join(*a, "|"), nil 60 | 61 | } 62 | 63 | type Metadata map[string]string 64 | 65 | func (m *Metadata) Scan(val any) error { 66 | s := val.([]uint8) 67 | return json.Unmarshal(s, m) 68 | } 69 | 70 | func (m *Metadata) Value() (driver.Value, error) { 71 | return json.Marshal(m) 72 | } 73 | 74 | // ModelGormTenant model gorm support multi tenant 75 | type ModelGormTenant struct { 76 | actions.ModelGorm 77 | // TenantID tenant id 78 | TenantID string `gorm:"column:tenant_id;type:varchar(64);not null;index;comment:租户ID" json:"tenantID"` 79 | } 80 | 81 | func (e *ModelGormTenant) BeforeCreate(tx *gorm.DB) (err error) { 82 | _, err = e.PrepareID(nil) 83 | if e.TenantID != "" { 84 | return nil 85 | } 86 | ctx, ok := tx.Statement.Context.(*gin.Context) 87 | if !ok { 88 | return fmt.Errorf("not gin context") 89 | } 90 | tenant, err := center.Default.GetTenant().GetTenant(ctx) 91 | if err != nil { 92 | return err 93 | } 94 | // tenantID Can only be assigned at creation time 95 | e.TenantID = tenant.GetID().(string) 96 | return err 97 | } 98 | 99 | func (e *ModelGormTenant) BeforeDelete(tx *gorm.DB) error { 100 | if e.TenantID != "" { 101 | return nil 102 | } 103 | ctx, ok := tx.Statement.Context.(*gin.Context) 104 | if !ok { 105 | return fmt.Errorf("not gin context") 106 | } 107 | tenant, err := center.Default.GetTenant().GetTenant(ctx) 108 | if err != nil { 109 | return err 110 | } 111 | tx = tx.Where("tenant_id = ?", tenant.GetID()) 112 | return nil 113 | } 114 | 115 | type ModelCreator struct { 116 | // CreatorID creator id 117 | CreatorID string `gorm:"column:creator_id;type:varchar(64);not null;index;comment:创建人ID" json:"creatorID"` 118 | } 119 | -------------------------------------------------------------------------------- /compose/consul/docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | # Production-grade Consul cluster (3 servers) with TLS and ACLs enabled. 4 | # Notes: 5 | # - Requires TLS certs present in ./certs (see README-PRODUCTION.md) 6 | # - Requires a gossip encryption key provided via environment (.env) 7 | # - Exposes only HTTPS (8501) on server-1 by default; adjust as needed 8 | 9 | x-consul-common: &consul-common 10 | image: hashicorp/consul:1.18 11 | restart: unless-stopped 12 | networks: 13 | - consul 14 | volumes: 15 | - ./config:/consul/config:ro 16 | - ./certs:/consul/certs:ro 17 | environment: 18 | # Make consul CLI inside the container use TLS by default (for healthchecks, etc.) 19 | CONSUL_HTTP_ADDR: https://127.0.0.1:8501 20 | CONSUL_CACERT: /consul/certs/ca.pem 21 | CONSUL_CLIENT_CERT: /consul/certs/server.pem 22 | CONSUL_CLIENT_KEY: /consul/certs/server-key.pem 23 | healthcheck: 24 | test: ["CMD", "sh", "-c", "consul info >/dev/null 2>&1"] 25 | interval: 15s 26 | timeout: 5s 27 | retries: 5 28 | 29 | services: 30 | consul-server-1: 31 | <<: *consul-common 32 | container_name: consul-server-1 33 | hostname: consul-server-1 34 | command: >- 35 | agent 36 | -server 37 | -bootstrap-expect=3 38 | -node=consul-server-1 39 | -datacenter=${CONSUL_DATACENTER:-dc1} 40 | -bind=0.0.0.0 41 | -client=0.0.0.0 42 | -encrypt=${GOSSIP_KEY} 43 | -retry-join=consul-server-1 44 | -retry-join=consul-server-2 45 | -retry-join=consul-server-3 46 | -config-dir=/consul/config 47 | ports: 48 | # Only expose HTTPS and DNS from a single node by default 49 | - "8501:8501" # HTTPS API/UI 50 | - "8502:8502" # gRPC (TLS) 51 | - "8600:8600/udp" # DNS (UDP) 52 | volumes: 53 | - consul-data-server1:/consul/data 54 | 55 | consul-server-2: 56 | <<: *consul-common 57 | container_name: consul-server-2 58 | hostname: consul-server-2 59 | command: >- 60 | agent 61 | -server 62 | -bootstrap-expect=3 63 | -node=consul-server-2 64 | -datacenter=${CONSUL_DATACENTER:-dc1} 65 | -bind=0.0.0.0 66 | -client=0.0.0.0 67 | -encrypt=${GOSSIP_KEY} 68 | -retry-join=consul-server-1 69 | -retry-join=consul-server-2 70 | -retry-join=consul-server-3 71 | -config-dir=/consul/config 72 | volumes: 73 | - consul-data-server2:/consul/data 74 | 75 | consul-server-3: 76 | <<: *consul-common 77 | container_name: consul-server-3 78 | hostname: consul-server-3 79 | command: >- 80 | agent 81 | -server 82 | -bootstrap-expect=3 83 | -node=consul-server-3 84 | -datacenter=${CONSUL_DATACENTER:-dc1} 85 | -bind=0.0.0.0 86 | -client=0.0.0.0 87 | -encrypt=${GOSSIP_KEY} 88 | -retry-join=consul-server-1 89 | -retry-join=consul-server-2 90 | -retry-join=consul-server-3 91 | -config-dir=/consul/config 92 | volumes: 93 | - consul-data-server3:/consul/data 94 | 95 | networks: 96 | consul: 97 | driver: bridge 98 | 99 | volumes: 100 | consul-data-server1: 101 | consul-data-server2: 102 | consul-data-server3: 103 | -------------------------------------------------------------------------------- /models/statistics.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/mss-boot-io/mss-boot-admin/center" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | /* 12 | * @Author: lwnmengjing 13 | * @Date: 2024/1/12 15:16:38 14 | * @Last Modified by: lwnmengjing 15 | * @Last Modified time: 2024/1/12 15:16:38 16 | */ 17 | 18 | type Statistics struct { 19 | ModelGormTenant 20 | // Name 统计名称 21 | Name string `gorm:"column:name;type:varchar(255);not null;comment:统计名称" json:"name"` 22 | // Type 统计类型 23 | Type string `gorm:"column:type;type:varchar(255);not null;comment:统计类型" json:"type"` 24 | // Value 统计值 * 100 25 | Value int `gorm:"column:value;type:int;not null;comment:统计值 * 100" json:"value"` 26 | // Time 统计时间 27 | Time string `gorm:"column:time;type:varchar(50);not null;comment:统计时间" json:"time"` 28 | } 29 | 30 | func (*Statistics) TableName() string { 31 | return "mss_boot_statistics" 32 | } 33 | 34 | func (e *Statistics) Calibrate(ctx *gin.Context, object center.StatisticsObject) error { 35 | s := &Statistics{ 36 | Name: object.StatisticsName(), 37 | Type: object.StatisticsType(), 38 | Time: object.StatisticsTime(), 39 | } 40 | err := center.GetDB(ctx, s).Where(s). 41 | FirstOrCreate(s).Error 42 | if err != nil { 43 | slog.Error("Statistics Calibrate", "error", err) 44 | return err 45 | } 46 | s.Value, err = object.StatisticsCalibrate() 47 | if err != nil { 48 | slog.Error("Statistics Calibrate", "error", err) 49 | return err 50 | } 51 | err = center.GetDB(ctx, s).Save(s).Error 52 | if err != nil { 53 | slog.Error("Statistics Calibrate", "error", err) 54 | return err 55 | } 56 | return nil 57 | } 58 | 59 | func (e *Statistics) NowIncrease(ctx *gin.Context, object center.StatisticsObject) error { 60 | s := &Statistics{ 61 | Name: object.StatisticsName(), 62 | Type: object.StatisticsType(), 63 | Time: object.StatisticsTime(), 64 | } 65 | err := center.GetDB(ctx, s).Where(s). 66 | FirstOrCreate(s).Error 67 | if err != nil { 68 | slog.Error("Statistics NowIncrease", "error", err) 69 | return err 70 | } 71 | if err != nil { 72 | slog.Error("Statistics NowIncrease", "error", err) 73 | return err 74 | } 75 | err = center.GetDB(ctx, s).Model(e). 76 | Update("value", gorm.Expr("value + ?", object.StatisticsStep())).Error 77 | if err != nil { 78 | slog.Error("Statistics NowIncrease", "error", err) 79 | return err 80 | } 81 | return nil 82 | } 83 | 84 | func (e *Statistics) NowReduce(ctx *gin.Context, object center.StatisticsObject) error { 85 | s := &Statistics{ 86 | Name: object.StatisticsName(), 87 | Type: object.StatisticsType(), 88 | Time: object.StatisticsTime(), 89 | } 90 | err := center.GetDB(ctx, s).Where(s). 91 | FirstOrCreate(s).Error 92 | if err != nil { 93 | slog.Error("Statistics NowReduce", "error", err) 94 | return err 95 | } 96 | if err != nil { 97 | slog.Error("Statistics NowReduce", "error", err) 98 | return err 99 | } 100 | err = center.GetDB(ctx, s).Model(s). 101 | Update("value", gorm.Expr("value - ?", object.StatisticsStep())).Error 102 | if err != nil { 103 | slog.Error("Statistics NowReduce", "error", err) 104 | return err 105 | } 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /apis/option.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/mss-boot-io/mss-boot-admin/center" 6 | "github.com/mss-boot-io/mss-boot/pkg/response" 7 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 8 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 9 | 10 | "github.com/mss-boot-io/mss-boot-admin/dto" 11 | "github.com/mss-boot-io/mss-boot-admin/middleware" 12 | "github.com/mss-boot-io/mss-boot-admin/models" 13 | ) 14 | 15 | /* 16 | * @Author: lwnmengjing 17 | * @Date: 2024/1/1 12:07:53 18 | * @Last Modified by: lwnmengjing 19 | * @Last Modified time: 2024/1/1 12:07:53 20 | */ 21 | 22 | func init() { 23 | e := &Option{ 24 | Simple: controller.NewSimple( 25 | controller.WithAuth(true), 26 | controller.WithModel(new(models.Option)), 27 | controller.WithSearch(new(dto.OptionSearch)), 28 | controller.WithModelProvider(actions.ModelProviderGorm), 29 | controller.WithScope(center.Default.Scope), 30 | ), 31 | } 32 | response.AppendController(e) 33 | } 34 | 35 | type Option struct { 36 | *controller.Simple 37 | } 38 | 39 | func (e *Option) Other(r *gin.RouterGroup) { 40 | r.Use(middleware.Auth.MiddlewareFunc()) 41 | } 42 | 43 | // Create 创建Option 44 | // @Summary 创建Option 45 | // @Description 创建Option 46 | // @Tags option 47 | // @Accept application/json 48 | // @Product application/json 49 | // @Param data body models.Option true "data" 50 | // @Success 201 {object} models.Option 51 | // @Router /admin/api/options [post] 52 | // @Security Bearer 53 | func (*Option) Create(*gin.Context) {} 54 | 55 | // Update 更新Option 56 | // @Summary 更新Option 57 | // @Description 更新Option 58 | // @Tags option 59 | // @Accept application/json 60 | // @Product application/json 61 | // @Param id path string true "id" 62 | // @Param data body models.Option true "data" 63 | // @Success 200 {object} models.Option 64 | // @Router /admin/api/options/{id} [put] 65 | // @Security Bearer 66 | func (*Option) Update(*gin.Context) {} 67 | 68 | // Delete 删除Option 69 | // @Summary 删除Option 70 | // @Description 删除Option 71 | // @Tags option 72 | // @Accept application/json 73 | // @Product application/json 74 | // @Param id path string true "id" 75 | // @Success 204 76 | // @Router /admin/api/options/{id} [delete] 77 | // @Security Bearer 78 | func (*Option) Delete(*gin.Context) {} 79 | 80 | // Get 获取Option 81 | // @Summary 获取Option 82 | // @Description 获取Option 83 | // @Tags option 84 | // @Accept application/json 85 | // @Product application/json 86 | // @Param id path string true "id" 87 | // @Success 200 {object} models.Option 88 | // @Router /admin/api/options/{id} [get] 89 | // @Security Bearer 90 | func (*Option) Get(*gin.Context) {} 91 | 92 | // List Option列表数据 93 | // @Summary Option列表数据 94 | // @Description Option列表数据 95 | // @Tags option 96 | // @Accept application/json 97 | // @Product application/json 98 | // @Param name query string false "name" 99 | // @Param status query string false "status" 100 | // @Param current query int false "current" 101 | // @Param pageSize query int false "pageSize" 102 | // @Success 200 {object} response.Page{data=[]models.Option} 103 | // @Router /admin/api/options [get] 104 | // @Security Bearer 105 | func (*Option) List(*gin.Context) {} 106 | -------------------------------------------------------------------------------- /config/pyroscope.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log/slog" 5 | "time" 6 | 7 | "github.com/grafana/pyroscope-go" 8 | "github.com/mss-boot-io/mss-boot-admin/center" 9 | ) 10 | 11 | /* 12 | * @Author: lwnmengjing 13 | * @Date: 2024/1/11 17:43:40 14 | * @Last Modified by: lwnmengjing 15 | * @Last Modified time: 2024/1/11 17:43:40 16 | */ 17 | 18 | type Pyroscope struct { 19 | Enabled bool `yaml:"enabled" json:"enabled"` 20 | ApplicationName string `yaml:"applicationName" json:"applicationName"` // e.g backend.purchases 21 | Tags map[string]string `yaml:"tags" json:"tags"` 22 | ServerAddress string `yaml:"serverAddress" json:"serverAddress"` // e.g http://pyroscope.services.internal:4040 23 | AuthToken string `yaml:"authToken" json:"authToken"` // specify this token when using pyroscope cloud 24 | BasicAuthUser string `yaml:"basicAuthUser" json:"basicAuthUser"` // http basic auth user 25 | BasicAuthPassword string `yaml:"basicAuthPassword" json:"basicAuthPassword"` // http basic auth password 26 | TenantID string `yaml:"tenantID" json:"tenantID"` 27 | UploadRate time.Duration `yaml:"uploadRate" json:"uploadRate"` 28 | Logger bool `yaml:"logger" json:"logger"` 29 | ProfileTypes []pyroscope.ProfileType `yaml:"profileTypes" json:"profileTypes"` 30 | DisableGCRuns bool `yaml:"disableGCRuns" json:"disableGCRuns"` // this will disable automatic runtime.GC runs between getting the heap profiles 31 | DisableAutomaticResets bool `yaml:"disableAutomaticResets" json:"disableAutomaticResets"` // disable automatic profiler reset every 10 seconds. Reset manually by calling Flush method 32 | HTTPHeaders map[string]string `yaml:"httpHeaders" json:"httpHeaders"` 33 | } 34 | 35 | func (e *Pyroscope) MergeTags(labels map[string]string) { 36 | for k, v := range e.Tags { 37 | labels[k] = v 38 | } 39 | e.Tags = labels 40 | } 41 | 42 | func (e *Pyroscope) Init() { 43 | if e.Enabled { 44 | c := pyroscope.Config{ 45 | Tags: e.Tags, 46 | ApplicationName: e.ApplicationName, 47 | ServerAddress: e.ServerAddress, 48 | BasicAuthUser: e.BasicAuthUser, 49 | BasicAuthPassword: e.BasicAuthPassword, 50 | TenantID: e.TenantID, 51 | UploadRate: e.UploadRate, 52 | ProfileTypes: e.ProfileTypes, 53 | DisableGCRuns: e.DisableGCRuns, 54 | DisableAutomaticResets: e.DisableAutomaticResets, 55 | HTTPHeaders: e.HTTPHeaders, 56 | } 57 | if len(c.ProfileTypes) == 0 { 58 | c.ProfileTypes = pyroscope.DefaultProfileTypes 59 | } 60 | if e.Tags == nil { 61 | e.Tags = make(map[string]string) 62 | } 63 | if _, ok := e.Tags["stage"]; !ok { 64 | c.Tags["stage"] = center.Stage() 65 | } 66 | profiler, err := pyroscope.Start(c) 67 | if err != nil { 68 | slog.Error("pyroscope start failed", "err", err) 69 | return 70 | } 71 | center.SetProfiler(profiler) 72 | slog.Info("pyroscope start success") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /center/type.go: -------------------------------------------------------------------------------- 1 | package center 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/mss-boot-io/mss-boot/core/server" 9 | "github.com/mss-boot-io/mss-boot/pkg/config/source" 10 | "github.com/mss-boot-io/mss-boot/pkg/security" 11 | "github.com/mss-boot-io/mss-boot/virtual/model" 12 | "google.golang.org/grpc" 13 | "gorm.io/gorm" 14 | "gorm.io/gorm/schema" 15 | ) 16 | 17 | /* 18 | * @Author: lwnmengjing 19 | * @Date: 2024/1/8 09:46:12 20 | * @Last Modified by: lwnmengjing 21 | * @Last Modified time: 2024/1/8 09:46:12 22 | */ 23 | 24 | type Center interface { 25 | NoticeImp 26 | TenantImp 27 | UserImp 28 | VirtualModelImp 29 | ConfigImp 30 | CustomConfigImp 31 | server.Manager 32 | gin.IRouter 33 | StageImp 34 | AppConfigImp 35 | UserConfigImp 36 | StatisticsImp 37 | MakeRouterImp 38 | GRPCClientImp 39 | VerifyCodeStoreImp 40 | } 41 | 42 | type GRPCClientImp interface { 43 | GetGRPCClient(string, ...grpc.DialOption) *grpc.ClientConn 44 | } 45 | 46 | type MakeRouterImp interface { 47 | SetFunc(...func(*gin.RouterGroup)) 48 | GetFunc() []func(*gin.RouterGroup) 49 | MakeRouter(*gin.RouterGroup) 50 | } 51 | 52 | type StageImp interface { 53 | Stage() string 54 | } 55 | 56 | type NoticeImp interface { 57 | List(ctx *gin.Context, userID string, page, size int) ([]NoticeImp, int, error) 58 | Unread(ctx *gin.Context, userID string) ([]NoticeImp, error) 59 | Read(ctx *gin.Context, userID string, ids []string) error 60 | Send(ctx *gin.Context, userID string, noticer NoticeImp) error 61 | } 62 | 63 | type TenantImp interface { 64 | Scope(ctx *gin.Context, table schema.Tabler) func(db *gorm.DB) *gorm.DB 65 | GetTenant(ctx *gin.Context) (TenantImp, error) 66 | GetDB(ctx *gin.Context, table schema.Tabler) *gorm.DB 67 | GetID() any 68 | GetDefault() bool 69 | } 70 | 71 | type TenantMigrator interface { 72 | Migrate(t TenantImp, tx *gorm.DB) error 73 | } 74 | 75 | type VirtualModelImp interface { 76 | GetModels(ctx *gin.Context) ([]VirtualModelImp, error) 77 | Make() *model.Model 78 | GetKey() string 79 | } 80 | 81 | type UserImp interface { 82 | security.Verifier 83 | } 84 | 85 | type ConfigImp interface { 86 | source.Entity 87 | Init(...source.Option) 88 | } 89 | 90 | type CustomConfigImp interface { 91 | ConfigImp 92 | } 93 | 94 | type AppConfigImp interface { 95 | SetAppConfig(ctx *gin.Context, key string, auth bool, value string) error 96 | GetAppConfig(ctx *gin.Context, key string) (string, bool) 97 | } 98 | 99 | type UserConfigImp interface { 100 | SetUserConfig(ctx *gin.Context, userID, key, value string) error 101 | GetUserConfig(ctx *gin.Context, userID, key string) (string, bool) 102 | } 103 | 104 | type StatisticsObject interface { 105 | StatisticsType() string 106 | StatisticsName() string 107 | StatisticsTime() string 108 | // StatisticsStep 统计步长 * 100 109 | StatisticsStep() int 110 | StatisticsCalibrate() (int, error) 111 | } 112 | 113 | type StatisticsImp interface { 114 | Calibrate(ctx *gin.Context, object StatisticsObject) error 115 | NowIncrease(ctx *gin.Context, object StatisticsObject) error 116 | NowReduce(ctx *gin.Context, object StatisticsObject) error 117 | } 118 | 119 | type VerifyCodeStoreImp interface { 120 | GenerateCode(ctx context.Context, key string, expire time.Duration) (string, error) 121 | VerifyCode(ctx context.Context, key, code string) (bool, error) 122 | } 123 | -------------------------------------------------------------------------------- /pkg/pack/zip.go: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lwnmengjing 3 | * @Date: 2022/10/27 16:40:42 4 | * @Last Modified by: lwnmengjing 5 | * @Last Modified time: 2022/10/27 16:40:42 6 | */ 7 | 8 | package pack 9 | 10 | import ( 11 | "archive/zip" 12 | "bytes" 13 | "fmt" 14 | "io" 15 | "os" 16 | "path/filepath" 17 | "strings" 18 | ) 19 | 20 | // Zip 文件压缩zip 21 | func Zip(root string, src []string, writer io.Writer, ignore ...string) error { 22 | // zip write 23 | zw := zip.NewWriter(writer) 24 | defer zw.Close() 25 | switch len(src) { 26 | case 0: 27 | src = []string{"."} 28 | } 29 | for i := range src { 30 | err := filepath.Walk(filepath.Join(root, src[i]), func(path string, info os.FileInfo, err error) error { 31 | if info.IsDir() { 32 | return nil 33 | } 34 | for j := range ignore { 35 | if strings.Index(path, ignore[j]) > -1 { 36 | return nil 37 | } 38 | } 39 | h, err := zip.FileInfoHeader(info) 40 | if err != nil { 41 | return err 42 | } 43 | h.Method = zip.Deflate 44 | h.Name = strings.ReplaceAll(strings.ReplaceAll(path, root, "")[1:], "\\", "/") 45 | h.Modified = info.ModTime() 46 | w, err := zw.CreateHeader(h) 47 | if err != nil { 48 | return err 49 | } 50 | fr, err := os.Open(path) 51 | if err != nil { 52 | return err 53 | } 54 | defer fr.Close() 55 | // 写信息头 56 | _, err = io.Copy(w, fr) 57 | if err != nil { 58 | return err 59 | } 60 | return nil 61 | }) 62 | if err != nil { 63 | return err 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | // Unzip 解压缩zip文件 70 | func Unzip(src, dst string) error { 71 | // zip reader 72 | or, err := zip.OpenReader(src) 73 | if err != nil { 74 | return err 75 | } 76 | defer or.Close() 77 | fmt.Sprintf("src: %v, dst: %v", src, dst) 78 | 79 | return unzipFromReader(or.Reader, dst) 80 | } 81 | 82 | func UnzipByContent(content []byte, dst string) error { 83 | // zip reader 84 | or, err := zip.NewReader(bytes.NewReader(content), int64(len(content))) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | return unzipFromReader(*or, dst) 90 | } 91 | 92 | func unzipFromReader(or zip.Reader, dst string) error { 93 | for _, f := range or.File { 94 | filePath := filepath.Join(dst, f.Name) 95 | fmt.Sprintf("unzipping file: %v", filePath) 96 | 97 | if !strings.HasPrefix(filePath, filepath.Clean(dst)+string(os.PathSeparator)) { 98 | fmt.Sprintf("invalid file path: %v", filePath) 99 | return nil 100 | } 101 | if f.FileInfo().IsDir() { 102 | fmt.Sprintf("creating directory: %v", filePath) 103 | os.MkdirAll(filePath, os.ModePerm) 104 | continue 105 | } 106 | 107 | if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { 108 | fmt.Sprintf("create file: %v failed", filePath) 109 | panic(err) 110 | } 111 | 112 | dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 113 | if err != nil { 114 | fmt.Sprintf("open dst file: %v failed", filePath) 115 | panic(err) 116 | } 117 | 118 | fileInArchive, err := f.Open() 119 | if err != nil { 120 | fmt.Sprintf("open src file: %v failed", f.Name) 121 | panic(err) 122 | } 123 | 124 | if _, err := io.Copy(dstFile, fileInArchive); err != nil { 125 | fmt.Sprintf("copy file to dst file: %v failed", dstFile) 126 | panic(err) 127 | } 128 | 129 | dstFile.Close() 130 | fileInArchive.Close() 131 | } 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /models/model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "log/slog" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/mss-boot-io/mss-boot-admin/center" 9 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 10 | 11 | "github.com/mss-boot-io/mss-boot/virtual/model" 12 | "gorm.io/gorm" 13 | "gorm.io/gorm/schema" 14 | 15 | "github.com/mss-boot-io/mss-boot-admin/pkg" 16 | ) 17 | 18 | /* 19 | * @Author: lwnmengjing 20 | * @Date: 2023/8/21 19:46:22 21 | * @Last Modified by: lwnmengjing 22 | * @Last Modified time: 2023/8/21 19:46:22 23 | */ 24 | 25 | type Model struct { 26 | actions.ModelGorm 27 | Name string `gorm:"column:name;type:varchar(255);not null;comment:名称" json:"name"` 28 | Description string `gorm:"column:description;type:text;not null;comment:描述" json:"description"` 29 | HardDeleted bool `gorm:"column:hard_deleted;size:1;not null;default:0;comment:是否硬删除" json:"hardDeleted"` 30 | Table string `gorm:"column:table_name;type:varchar(255);not null;comment:表名" json:"table"` 31 | Path string `gorm:"column:path;type:varchar(255);not null;comment:http路径" json:"path"` 32 | Fields []*Field `gorm:"foreignKey:ModelID;references:ID" json:"fields"` 33 | MultiTenant bool `gorm:"column:multi_tenant;size:1;default:0;comment:多租户" json:"multiTenant"` 34 | Auth bool `gorm:"column:auth;size:1;default:0;comment:是否需要认证" json:"auth"` 35 | GeneratedData bool `gorm:"column:generated_data;size:1;not null;default:0;comment:是否生成数据" json:"generatedData"` 36 | } 37 | 38 | func (*Model) TableName() string { 39 | return "mss_boot_models" 40 | } 41 | 42 | func (e *Model) BeforeCreate(_ *gorm.DB) error { 43 | _ = e.ModelGorm.BeforeCreate(nil) 44 | if e.Path == "" { 45 | e.Path = pkg.Pluralize(strings.ReplaceAll(e.Table, "_", "-")) 46 | } 47 | return nil 48 | } 49 | 50 | func (e *Model) MakeVirtualModel() *model.Model { 51 | mm := &model.Model{ 52 | Name: e.Name, 53 | Table: e.Table, 54 | Description: e.Description, 55 | HardDeleted: e.HardDeleted, 56 | MultiTenant: e.MultiTenant, 57 | Auth: e.Auth, 58 | Fields: make([]*model.Field, len(e.Fields)), 59 | } 60 | for i := range e.Fields { 61 | mm.Fields[i] = &model.Field{ 62 | Name: e.Fields[i].Name, 63 | JsonTag: e.Fields[i].JsonTag, 64 | DataType: schema.DataType(e.Fields[i].Type), 65 | PrimaryKey: e.Fields[i].PrimaryKey, 66 | DefaultValue: e.Fields[i].Default, 67 | NotNull: e.Fields[i].NotNull, 68 | Unique: e.Fields[i].UniqueIndex, 69 | Index: e.Fields[i].Index, 70 | Comment: e.Fields[i].Comment, 71 | Size: e.Fields[i].Size, 72 | Search: e.Fields[i].Search, 73 | } 74 | } 75 | mm.Init() 76 | return mm 77 | } 78 | 79 | func (e *Model) Make() *model.Model { 80 | return e.MakeVirtualModel() 81 | } 82 | 83 | // GetModels get all virtual models info 84 | func (e *Model) GetModels(ctx *gin.Context) ([]center.VirtualModelImp, error) { 85 | var models []*Model 86 | err := center.Default.GetDB(ctx, e).Preload("Fields").Find(&models).Error 87 | if err != nil { 88 | slog.Error("get models failed", "err", err) 89 | return nil, err 90 | } 91 | vms := make([]center.VirtualModelImp, len(models)) 92 | for i := range models { 93 | vms[i] = models[i] 94 | } 95 | return vms, nil 96 | } 97 | 98 | func (e *Model) GetKey() string { 99 | return e.Path 100 | } 101 | -------------------------------------------------------------------------------- /apis/app_config.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/mss-boot-io/mss-boot/pkg/response" 8 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 9 | 10 | "github.com/mss-boot-io/mss-boot-admin/dto" 11 | "github.com/mss-boot-io/mss-boot-admin/service" 12 | ) 13 | 14 | /* 15 | * @Author: lwnmengjing 16 | * @Date: 2024/1/11 17:36:55 17 | * @Last Modified by: lwnmengjing 18 | * @Last Modified time: 2024/1/11 17:36:55 19 | */ 20 | 21 | func init() { 22 | e := &AppConfig{ 23 | Simple: controller.NewSimple(), 24 | } 25 | response.AppendController(e) 26 | } 27 | 28 | type AppConfig struct { 29 | *controller.Simple 30 | service service.AppConfig 31 | } 32 | 33 | func (e *AppConfig) GetAction(string) response.Action { 34 | return nil 35 | } 36 | 37 | func (e *AppConfig) Other(r *gin.RouterGroup) { 38 | r.GET("/app-configs/:group", response.AuthHandler, e.Group) 39 | r.PUT("/app-configs/:group", response.AuthHandler, e.Control) 40 | r.GET("/app-configs/profile", e.Profile) 41 | } 42 | 43 | // Profile 获取应用配置 44 | // @Summary 获取应用配置 45 | // @Description 获取应用配置 46 | // @Tags app-config 47 | // @Accept application/json 48 | // @Product application/json 49 | // @Success 200 {object} map[string]map[string]string 50 | // @Router /admin/api/app-configs/profile [get] 51 | // @Security Bearer 52 | func (e *AppConfig) Profile(ctx *gin.Context) { 53 | api := response.Make(ctx) 54 | verify := response.VerifyHandler(ctx) 55 | profile, err := e.service.Profile(ctx, verify != nil) 56 | if err != nil { 57 | api.AddError(err).Log.Error("get app config profile error") 58 | api.Err(http.StatusInternalServerError) 59 | return 60 | } 61 | api.OK(profile) 62 | } 63 | 64 | // Group 应用配置分组 65 | // @Summary 应用配置分组 66 | // @Description 应用配置分组 67 | // @Tags app-config 68 | // @Accept application/json 69 | // @Product application/json 70 | // @Param group path string true "group" 71 | // @Success 200 {object} map[string]models.AppConfig 72 | // @Router /admin/api/app-configs/{group} [get] 73 | // @Security Bearer 74 | func (e *AppConfig) Group(ctx *gin.Context) { 75 | api := response.Make(ctx) 76 | req := &dto.AppConfigGroupRequest{} 77 | if err := api.Bind(req).Error; err != nil { 78 | api.Err(http.StatusUnprocessableEntity) 79 | return 80 | } 81 | result, err := e.service.Group(ctx, req.Group) 82 | if err != nil { 83 | api.AddError(err).Log.Error("get app config group error") 84 | api.Err(http.StatusInternalServerError) 85 | return 86 | } 87 | api.OK(result) 88 | } 89 | 90 | // Control 应用配置控制 91 | // @Summary 应用配置控制 92 | // @Description 应用配置控制 93 | // @Tags app-config 94 | // @Accept application/json 95 | // @Product application/json 96 | // @Param group path string true "group" 97 | // @Param data body dto.AppConfigControlRequest true "data" 98 | // @Success 200 99 | // @Router /admin/api/app-configs/{group} [put] 100 | // @Security Bearer 101 | func (e *AppConfig) Control(ctx *gin.Context) { 102 | api := response.Make(ctx) 103 | req := &dto.AppConfigControlRequest{ 104 | Data: make(map[string]any), 105 | } 106 | if err := api.Bind(req).Error; err != nil { 107 | api.Err(http.StatusUnprocessableEntity) 108 | return 109 | } 110 | err := e.service.CreateOrUpdate(ctx, req.Group, req.Data) 111 | if err != nil { 112 | api.AddError(err).Log.Error("update app config error") 113 | api.Err(http.StatusInternalServerError) 114 | return 115 | } 116 | api.OK(nil) 117 | } 118 | -------------------------------------------------------------------------------- /apis/system_config.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/mss-boot-io/mss-boot/pkg/response" 8 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 9 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 10 | 11 | "github.com/mss-boot-io/mss-boot-admin/center" 12 | "github.com/mss-boot-io/mss-boot-admin/dto" 13 | "github.com/mss-boot-io/mss-boot-admin/models" 14 | ) 15 | 16 | /* 17 | * @Author: lwnmengjing 18 | * @Date: 2023/12/20 17:52:05 19 | * @Last Modified by: lwnmengjing 20 | * @Last Modified time: 2023/12/20 17:52:05 21 | */ 22 | 23 | func init() { 24 | e := &SystemConfig{ 25 | Simple: controller.NewSimple( 26 | controller.WithAuth(true), 27 | controller.WithModel(new(models.SystemConfig)), 28 | controller.WithSearch(new(dto.SystemConfigSearch)), 29 | controller.WithHandlers(gin.HandlersChain{ 30 | func(ctx *gin.Context) { 31 | api := response.Make(ctx) 32 | tenant, err := center.GetTenant().GetTenant(ctx) 33 | if err != nil { 34 | api.AddError(err) 35 | api.Err(http.StatusUnauthorized) 36 | ctx.Abort() 37 | return 38 | } 39 | if !tenant.GetDefault() { 40 | api.Err(http.StatusUnauthorized) 41 | ctx.Abort() 42 | return 43 | } 44 | ctx.Next() 45 | }, 46 | }), 47 | controller.WithModelProvider(actions.ModelProviderGorm), 48 | ), 49 | } 50 | response.AppendController(e) 51 | } 52 | 53 | type SystemConfig struct { 54 | *controller.Simple 55 | } 56 | 57 | // Create 创建系统配置 58 | // @Summary 创建系统配置 59 | // @Description 创建系统配置 60 | // @Tags system_config 61 | // @Accept application/json 62 | // @Produce application/json 63 | // @Param data body models.SystemConfig true "data" 64 | // @Success 201 {object} models.SystemConfig 65 | // @Router /admin/api/system-configs [post] 66 | // @Security Bearer 67 | func (*SystemConfig) Create(*gin.Context) {} 68 | 69 | // Update 更新系统配置 70 | // @Summary 更新系统配置 71 | // @Description 更新系统配置 72 | // @Tags system_config 73 | // @Accept application/json 74 | // @Produce application/json 75 | // @Param id path string true "id" 76 | // @Param data body models.SystemConfig true "data" 77 | // @Success 200 {object} models.SystemConfig 78 | // @Router /admin/api/system-configs/{id} [put] 79 | // @Security Bearer 80 | func (*SystemConfig) Update(*gin.Context) {} 81 | 82 | // Delete 删除系统配置 83 | // @Summary 删除系统配置 84 | // @Description 删除系统配置 85 | // @Tags system_config 86 | // @Param id path string true "id" 87 | // @Success 204 88 | // @Router /admin/api/system-configs/{id} [delete] 89 | // @Security Bearer 90 | func (*SystemConfig) Delete(*gin.Context) {} 91 | 92 | // Get 获取系统配置 93 | // @Summary 获取系统配置 94 | // @Description 获取系统配置 95 | // @Tags system_config 96 | // @Param id path string true "id" 97 | // @Success 200 {object} models.SystemConfig 98 | // @Router /admin/api/system-configs/{id} [get] 99 | // @Security Bearer 100 | func (*SystemConfig) Get(*gin.Context) {} 101 | 102 | // List 系统配置列表数据 103 | // @Summary 系统配置列表数据 104 | // @Description 系统配置列表数据 105 | // @Tags system_config 106 | // @Accept application/json 107 | // @Produce application/json 108 | // @Param current query int false "current" 109 | // @Param pageSize query int false "pageSize" 110 | // @Success 200 {object} response.Page{data=[]models.SystemConfig} 111 | // @Router /admin/api/system-configs [get] 112 | // @Security Bearer 113 | func (*SystemConfig) List(*gin.Context) {} 114 | -------------------------------------------------------------------------------- /apis/virtual.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "sort" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/mss-boot-io/mss-boot/pkg/config/gormdb" 10 | "github.com/mss-boot-io/mss-boot/pkg/response" 11 | "github.com/mss-boot-io/mss-boot/virtual/action" 12 | vapi "github.com/mss-boot-io/mss-boot/virtual/api" 13 | 14 | "github.com/mss-boot-io/mss-boot-admin/dto" 15 | "github.com/mss-boot-io/mss-boot-admin/models" 16 | ) 17 | 18 | /* 19 | * @Author: lwnmengjing 20 | * @Date: 2024/1/2 16:45:08 21 | * @Last Modified by: lwnmengjing 22 | * @Last Modified time: 2024/1/2 16:45:08 23 | */ 24 | 25 | func init() { 26 | base := action.GetBase() 27 | base.TenantIDFunc = models.TenantIDScope 28 | //center.Default.GetTenant().GetTenant() 29 | e := &Virtual{ 30 | Virtual: vapi.NewVirtual( 31 | base, 32 | //controller.WithAuth(true), 33 | ), 34 | } 35 | response.AppendController(e) 36 | } 37 | 38 | type Virtual struct { 39 | *vapi.Virtual 40 | } 41 | 42 | func (e *Virtual) Other(r *gin.RouterGroup) { 43 | r.GET(fmt.Sprintf("/documentation/:%s", e.GetKey()), e.Documentation) 44 | } 45 | 46 | // Documentation 文档 47 | // @Summary 文档 48 | // @Description 文档 49 | // @Tags virtual 50 | // @Accept application/json 51 | // @Produce application/json 52 | // @Param key path string true "key" 53 | // @Success 200 {object} dto.VirtualModelObject 54 | // @Router /admin/api/documentation/{key} [get] 55 | func (e *Virtual) Documentation(ctx *gin.Context) { 56 | api := response.Make(ctx) 57 | vm := &models.Model{} 58 | err := gormdb.DB.Model(vm). 59 | Preload("Fields"). 60 | Where("path = ?", ctx.Param(e.GetKey())). 61 | First(vm).Error 62 | if err != nil { 63 | api.AddError(err).Log.Error("get model error", "key", ctx.Param(e.GetKey())) 64 | api.Err(http.StatusInternalServerError) 65 | return 66 | } 67 | object := &dto.VirtualModelObject{ 68 | Name: vm.Name, 69 | Columns: make([]*dto.ColumnType, len(vm.Fields)), 70 | } 71 | fields := models.Fields(vm.Fields) 72 | sort.Sort(fields) 73 | for i := range vm.Fields { 74 | object.Columns[i] = &dto.ColumnType{ 75 | Title: vm.Fields[i].Label, 76 | DataIndex: vm.Fields[i].Name, 77 | PK: vm.Fields[i].PrimaryKey != "", 78 | } 79 | if vm.Fields[i].FieldFrontend != nil { 80 | object.Columns[i].ValueType = vm.Fields[i].FieldFrontend.FormComponent 81 | object.Columns[i].HideInTable = vm.Fields[i].FieldFrontend.HideInTable 82 | object.Columns[i].HideInDescriptions = vm.Fields[i].FieldFrontend.HideInDescriptions 83 | object.Columns[i].HideInForm = vm.Fields[i].FieldFrontend.HideInForm 84 | object.Columns[i].ValidateRules = vm.Fields[i].FieldFrontend.Rules 85 | } 86 | if vm.Fields[i].ValueEnumName != "" { 87 | option := &models.Option{} 88 | err = gormdb.DB.Model(option). 89 | Where("id = ?", vm.Fields[i].ValueEnumName). 90 | First(option).Error 91 | if err != nil { 92 | api.AddError(err).Log.Error("get option error", "name", vm.Fields[i].ValueEnumName) 93 | api.Err(http.StatusInternalServerError) 94 | return 95 | } 96 | object.Columns[i].ValueEnum = make(map[string]dto.ValueEnumType) 97 | if option.Items != nil { 98 | for j := range *option.Items { 99 | object.Columns[i].ValueEnum[(*option.Items)[j].Value] = dto.ValueEnumType{ 100 | Text: (*option.Items)[j].Label, 101 | Status: (*option.Items)[j].Value, 102 | Color: (*option.Items)[j].Color, 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | api.OK(object) 110 | } 111 | -------------------------------------------------------------------------------- /models/app_config.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | "gorm.io/gorm/clause" 10 | 11 | "github.com/mss-boot-io/mss-boot-admin/center" 12 | ) 13 | 14 | /* 15 | * @Author: lwnmengjing 16 | * @Date: 2024/1/11 11:58:29 17 | * @Last Modified by: lwnmengjing 18 | * @Last Modified time: 2024/1/11 11:58:29 19 | */ 20 | 21 | type AppConfig struct { 22 | ModelGormTenant 23 | // Name 名称 24 | Name string `gorm:"column:name;size:128;index;default:'';not null" json:"name" binding:"required"` 25 | // Group 分组 26 | Group string `gorm:"column:group;size:128;index;default:'';not null" json:"group" binding:"required"` 27 | // Value 值 28 | Value string `gorm:"column:value;size:255;default:'';not null" json:"value"` 29 | // Auth 是否需要认证 如果为true,只有登录后才会返回 30 | Auth bool `gorm:"column:auth;default:false;not null" json:"auth"` 31 | } 32 | 33 | func (*AppConfig) TableName() string { 34 | return "mss_boot_app_configs" 35 | } 36 | 37 | func (e *AppConfig) SetAppConfig(ctx *gin.Context, key string, auth bool, value string) error { 38 | if key == "" { 39 | return nil 40 | } 41 | 42 | var group string 43 | keys := strings.Split(key, ":") 44 | if len(keys) > 1 { 45 | group = keys[0] 46 | key = strings.Join(keys[1:], ":") 47 | } 48 | c := &AppConfig{ 49 | Group: group, 50 | Name: key, 51 | } 52 | t, err := center.GetTenant().GetTenant(ctx) 53 | if err != nil { 54 | return err 55 | } 56 | //set cache 57 | 58 | c.Auth = auth 59 | c.Value = value 60 | c.UpdatedAt = time.Now() 61 | if center.GetCache() != nil { 62 | err = center.GetCache().HSet(ctx, 63 | fmt.Sprintf("%s:%s", t.GetID(), "appConfig"), 64 | fmt.Sprintf("%s:%s", c.Group, c.Name), value).Err() 65 | if err != nil { 66 | return err 67 | } 68 | } 69 | return center.GetTenant().GetDB(ctx, e). 70 | Clauses(clause.OnConflict{ 71 | Columns: []clause.Column{ 72 | {Name: "tenant_id"}, 73 | {Name: "name"}, 74 | {Name: "group"}, 75 | }, 76 | DoUpdates: clause.AssignmentColumns( 77 | []string{ 78 | "auth", 79 | "value", 80 | "updated_at"}), 81 | }). 82 | Create(c).Error 83 | } 84 | 85 | func getAppConfig(ctx *gin.Context, key string) (*AppConfig, error) { 86 | c := &AppConfig{} 87 | if key == "" { 88 | return nil, fmt.Errorf("key is empty") 89 | } 90 | 91 | var group string 92 | keys := strings.Split(key, ":") 93 | if len(keys) > 1 { 94 | group = keys[0] 95 | key = strings.Join(keys[1:], ":") 96 | } 97 | t, err := center.GetTenant().GetTenant(ctx) 98 | if err != nil { 99 | return nil, err 100 | } 101 | if center.GetCache() == nil { 102 | v, _ := center.GetCache().HGet(ctx, 103 | fmt.Sprintf("%s:%s", t.GetID(), "appConfig"), 104 | fmt.Sprintf("%s:%s", group, key)).Result() 105 | if v != "" { 106 | c.Group = group 107 | c.Name = key 108 | c.Value = v 109 | return c, nil 110 | } 111 | } 112 | condition := &AppConfig{ 113 | Group: group, 114 | Name: key, 115 | } 116 | err = center.GetTenant().GetDB(ctx, c). 117 | Model(condition). 118 | Where(condition). 119 | First(c).Error 120 | if err != nil { 121 | return nil, err 122 | } 123 | _ = center.GetCache().HSet(ctx, 124 | fmt.Sprintf("%s:%s", t.GetID(), "appConfig"), 125 | fmt.Sprintf("%s:%s", c.Group, c.Name), 126 | c.Value, -1).Err() 127 | return c, nil 128 | } 129 | 130 | func (e *AppConfig) GetAppConfig(ctx *gin.Context, key string) (string, bool) { 131 | c, err := getAppConfig(ctx, key) 132 | if err != nil { 133 | return "", false 134 | } 135 | return c.Value, true 136 | } 137 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '.github/**' 8 | - '.gitignore' 9 | branches: 10 | - main 11 | tags: 12 | - 'v*.*.*' 13 | pull_request: 14 | branches: 15 | - main 16 | 17 | env: 18 | # Use docker.io for Docker Hub if empty 19 | REGISTRY: ghcr.io 20 | # github.repository as / 21 | IMAGE_NAME: ${{ github.repository }} 22 | 23 | jobs: 24 | build: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: Setup golang 31 | uses: actions/setup-go@v5 32 | with: 33 | go-version: 1.25 34 | - name: Start Redis 35 | uses: supercharge/redis-github-action@1.8.0 36 | with: 37 | redis-version: 7 38 | - name: Install dependencies 39 | run: make deps 40 | - name: Unit Test 41 | run: make test 42 | 43 | - name: Convert coverage report to table 44 | run: go tool cover -func=coverage.out | tail -n +2 | awk '{print "|",$1,"|",$3,"|"}' > coverage_table.md 45 | 46 | - name: Comment on PR with coverage table 47 | if: github.event_name == 'pull_request' 48 | working-directory: cmd/tools/pr 49 | run: go mod tidy && go run main.go 50 | continue-on-error: true 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | REPO_NAME: ${{ github.repository }} 54 | PR_NUMBER: ${{ github.event.number }} 55 | COVERAGE_FILE: ../../../coverage_table.md 56 | - name: Build 57 | run: make build 58 | - name: Vendor 59 | run: go mod vendor 60 | 61 | # Login against a Docker registry except on PR 62 | # https://github.com/docker/login-action 63 | - name: Log into registry ${{ env.REGISTRY }} 64 | if: github.event_name != 'pull_request' 65 | uses: docker/login-action@v3 66 | with: 67 | registry: ${{ env.REGISTRY }} 68 | username: ${{ github.actor }} 69 | password: ${{ secrets.GITHUB_TOKEN }} 70 | 71 | - name: Set up QEMU 72 | if: github.event_name != 'pull_request' 73 | uses: docker/setup-qemu-action@v3 74 | 75 | - name: Set up Docker Buildx 76 | if: github.event_name != 'pull_request' 77 | uses: docker/setup-buildx-action@v3 78 | 79 | # Extract metadata (tags, labels) for Docker 80 | # https://github.com/docker/metadata-action 81 | - name: Extract Docker metadata 82 | id: meta 83 | if: github.event_name != 'pull_request' 84 | uses: docker/metadata-action@v5 85 | with: 86 | images: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}' 87 | flavor: | 88 | latest=auto 89 | tags: | 90 | type=schedule 91 | type=ref,event=tag 92 | type=sha,prefix=,format=long,enable=true,priority=100 93 | 94 | # Build and push Docker image with Buildx (don't push on PR) 95 | # https://github.com/docker/build-push-action 96 | - name: Build and push Docker image 97 | if: github.event_name != 'pull_request' 98 | uses: docker/build-push-action@v5 99 | with: 100 | context: . 101 | file: Dockerfile 102 | push: ${{ github.event_name != 'pull_request' }} 103 | tags: ${{ steps.meta.outputs.tags }} 104 | labels: ${{ steps.meta.outputs.labels }} 105 | platforms: linux/amd64 -------------------------------------------------------------------------------- /apis/user_config.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/mss-boot-io/mss-boot/pkg/response" 8 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 9 | 10 | "github.com/mss-boot-io/mss-boot-admin/dto" 11 | "github.com/mss-boot-io/mss-boot-admin/middleware" 12 | "github.com/mss-boot-io/mss-boot-admin/service" 13 | ) 14 | 15 | /* 16 | * @Author: lwnmengjing 17 | * @Date: 2024/3/2 00:41:41 18 | * @Last Modified by: lwnmengjing 19 | * @Last Modified time: 2024/3/2 00:41:41 20 | */ 21 | 22 | func init() { 23 | e := &UserConfig{ 24 | Simple: controller.NewSimple(), 25 | } 26 | response.AppendController(e) 27 | } 28 | 29 | type UserConfig struct { 30 | *controller.Simple 31 | service service.UserConfig 32 | } 33 | 34 | func (e *UserConfig) GetAction(string) response.Action { 35 | return nil 36 | } 37 | 38 | func (e *UserConfig) Other(r *gin.RouterGroup) { 39 | r.GET("/user-configs/:group", response.AuthHandler, e.Group) 40 | r.PUT("/user-configs/:group", response.AuthHandler, e.Control) 41 | r.GET("/user-configs/profile", e.Profile) 42 | } 43 | 44 | // Profile 用户配置 45 | // @Summary 用户配置 46 | // @Description 用户配置 47 | // @Tags user-config 48 | // @Accept application/json 49 | // @Product application/json 50 | // @Success 200 {object} map[string]map[string]string 51 | // @Router /admin/api/user-configs/profile [get] 52 | // @Security Bearer 53 | func (e *UserConfig) Profile(ctx *gin.Context) { 54 | api := response.Make(ctx) 55 | verify := response.VerifyHandler(ctx) 56 | if verify == nil { 57 | api.OK(nil) 58 | return 59 | } 60 | result, err := e.service.Profile(ctx, verify.GetTenantID(), verify.GetUserID()) 61 | if err != nil { 62 | api.AddError(err).Log.Error("get user config error") 63 | api.Err(http.StatusInternalServerError) 64 | return 65 | } 66 | api.OK(result) 67 | } 68 | 69 | // Group 用户配置分组 70 | // @Summary 用户配置分组 71 | // @Description 用户配置分组 72 | // @Tags user-config 73 | // @Accept application/json 74 | // @Product application/json 75 | // @Param group path string true "group" 76 | // @Success 200 {object} map[string]string 77 | // @Router /admin/api/user-configs/{group} [get] 78 | // @Security Bearer 79 | func (e *UserConfig) Group(ctx *gin.Context) { 80 | api := response.Make(ctx) 81 | verify := middleware.GetVerify(ctx) 82 | req := &dto.UserConfigGroupRequest{} 83 | if err := api.Bind(req).Error; err != nil { 84 | api.Err(http.StatusUnprocessableEntity) 85 | return 86 | } 87 | result, err := e.service.Group(ctx, verify.GetUserID(), req.Group) 88 | if err != nil { 89 | api.AddError(err).Log.Error("get user config error") 90 | api.Err(http.StatusInternalServerError) 91 | return 92 | } 93 | api.OK(result) 94 | } 95 | 96 | // Control 用户配置控制 97 | // @Summary 用户配置控制 98 | // @Description 用户配置控制 99 | // @Tags user-config 100 | // @Accept application/json 101 | // @Product application/json 102 | // @Param group path string true "group" 103 | // @Param data body dto.UserConfigControlRequest true "data" 104 | // @Success 200 105 | // @Router /admin/api/user-configs/{group} [put] 106 | // @Security Bearer 107 | func (e *UserConfig) Control(ctx *gin.Context) { 108 | api := response.Make(ctx) 109 | verify := middleware.GetVerify(ctx) 110 | req := &dto.UserConfigControlRequest{} 111 | if err := api.Bind(req).Error; err != nil { 112 | api.Err(http.StatusUnprocessableEntity) 113 | return 114 | } 115 | err := e.service.CreateOrUpdate(ctx, verify.GetUserID(), req.Group, req.Data) 116 | if err != nil { 117 | api.AddError(err).Log.Error("control user config error") 118 | api.Err(http.StatusInternalServerError) 119 | return 120 | } 121 | api.OK(nil) 122 | } 123 | -------------------------------------------------------------------------------- /notice/email/send.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "fmt" 7 | "html/template" 8 | "log/slog" 9 | "net/mail" 10 | "net/smtp" 11 | "time" 12 | ) 13 | 14 | /* 15 | * @Author: lwnmengjing 16 | * @Date: 2024/8/13 17:04:03 17 | * @Last Modified by: lwnmengjing 18 | * @Last Modified time: 2024/8/13 17:04:03 19 | */ 20 | 21 | //go:embed *.html 22 | var FS embed.FS 23 | 24 | type SendType string 25 | 26 | const ( 27 | RegisterSender SendType = "register" 28 | LoginSender SendType = "login" 29 | ResetPasswordSender SendType = "resetPassword" 30 | ) 31 | 32 | func (s SendType) String() string { 33 | return string(s) 34 | } 35 | 36 | type VerifyCodeSender func(smtpHost, smtpPort, from, password, username, to, code, organization string) error 37 | 38 | var Sender = map[SendType]VerifyCodeSender{ 39 | RegisterSender: SendRegisterVerifyCode, 40 | LoginSender: SendLoginVerifyCode, 41 | ResetPasswordSender: SendResetPasswordVerifyCode, 42 | } 43 | 44 | // SendRegisterVerifyCode 发送注册验证码 45 | func SendRegisterVerifyCode(smtpHost, smtpPort, from, password, username, to, code, organization string) error { 46 | return sendVerifyCode("register_verify_code.html", smtpHost, smtpPort, from, password, username, to, code, organization) 47 | } 48 | 49 | // SendLoginVerifyCode 发送登录验证码 50 | func SendLoginVerifyCode(smtpHost, smtpPort, from, password, username, to, code, organization string) error { 51 | return sendVerifyCode("login_verify_code.html", smtpHost, smtpPort, from, password, username, to, code, organization) 52 | } 53 | 54 | // SendResetPasswordVerifyCode 发送重置密码验证码 55 | func SendResetPasswordVerifyCode(smtpHost, smtpPort, from, password, username, to, code, organization string) error { 56 | return sendVerifyCode("password_reset_code.html", smtpHost, smtpPort, from, password, username, to, code, organization) 57 | } 58 | 59 | func sendVerifyCode(temp, smtpHost, smtpPort, from, password, username, to, code, organization string) error { 60 | rb, err := FS.ReadFile(temp) 61 | if err != nil { 62 | return err 63 | } 64 | // html template parse 65 | tmpl, err := template.New("email").Parse(string(rb)) 66 | if err != nil { 67 | return err 68 | } 69 | data := map[string]any{ 70 | "Code": code, 71 | "Year": time.Now().Year(), 72 | "Organization": organization, 73 | } 74 | var body bytes.Buffer 75 | if err = tmpl.Execute(&body, data); err != nil { 76 | return err 77 | } 78 | // 发件人信息 79 | fromAddress := mail.Address{Name: organization, Address: from} 80 | // 收件人信息 81 | toAddress := mail.Address{Name: username, Address: to} 82 | 83 | // 邮件头信息 84 | headers := make(map[string]string) 85 | headers["From"] = fromAddress.String() 86 | headers["To"] = toAddress.String() 87 | headers["Subject"] = "Your verification code is " + code + " (valid for 5 minutes)" 88 | headers["Date"] = time.Now().Format(time.RFC1123Z) 89 | headers["MIME-Version"] = "1.0" 90 | headers["Content-Type"] = `text/html; charset="UTF-8"` 91 | // 构建邮件内容 92 | var msg bytes.Buffer 93 | for k, v := range headers { 94 | _, err = fmt.Fprintf(&msg, "%s: %s\r\n", k, v) 95 | if err != nil { 96 | return err 97 | } 98 | } 99 | _, err = fmt.Fprintf(&msg, "\r\n%s", body.String()) 100 | if err != nil { 101 | return err 102 | } 103 | // SMTP 配置 104 | auth := smtp.PlainAuth("", fromAddress.Address, password, smtpHost) 105 | 106 | // 发送邮件 107 | err = smtp.SendMail(smtpHost+":"+smtpPort, auth, fromAddress.Address, []string{toAddress.Address}, msg.Bytes()) 108 | if err != nil { 109 | slog.Error("Failed to send email", slog.Any("error", err)) 110 | return err 111 | } 112 | slog.Info("Email sent successfully!") 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /apis/tenant.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/mss-boot-io/mss-boot/pkg/response" 8 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 9 | "github.com/mss-boot-io/mss-boot/pkg/response/controller" 10 | "gorm.io/gorm" 11 | "gorm.io/gorm/schema" 12 | 13 | "github.com/mss-boot-io/mss-boot-admin/center" 14 | "github.com/mss-boot-io/mss-boot-admin/dto" 15 | "github.com/mss-boot-io/mss-boot-admin/models" 16 | ) 17 | 18 | /* 19 | * @Author: lwnmengjing 20 | * @Date: 2024/1/8 18:14:12 21 | * @Last Modified by: lwnmengjing 22 | * @Last Modified time: 2024/1/8 18:14:12 23 | */ 24 | 25 | func init() { 26 | e := &Tenant{ 27 | Simple: controller.NewSimple( 28 | controller.WithAuth(true), 29 | controller.WithModel(new(models.Tenant)), 30 | controller.WithSearch(new(dto.TenantSearch)), 31 | controller.WithModelProvider(actions.ModelProviderGorm), 32 | controller.WithHandlers(gin.HandlersChain{ 33 | func(ctx *gin.Context) { 34 | api := response.Make(ctx) 35 | verify := response.VerifyHandler(ctx) 36 | if verify == nil { 37 | api.Err(http.StatusUnauthorized) 38 | ctx.Abort() 39 | return 40 | } 41 | tenant, err := center.GetTenant().GetTenant(ctx) 42 | if err != nil { 43 | api.AddError(err) 44 | api.Err(http.StatusUnauthorized) 45 | ctx.Abort() 46 | return 47 | } 48 | if tenant.GetID() != verify.GetTenantID() || !tenant.GetDefault() { 49 | api.Err(http.StatusUnauthorized) 50 | ctx.Abort() 51 | return 52 | } 53 | ctx.Next() 54 | }, 55 | }), 56 | controller.WithAfterCreate(func(ctx *gin.Context, db *gorm.DB, m schema.Tabler) error { 57 | return center.GetTenantMigrator().Migrate(m.(center.TenantImp), db) 58 | //return nil 59 | }), 60 | ), 61 | } 62 | response.AppendController(e) 63 | } 64 | 65 | type Tenant struct { 66 | *controller.Simple 67 | } 68 | 69 | // Create 创建租户 70 | // @Summary 创建租户 71 | // @Description 创建租户 72 | // @Tags tenant 73 | // @Accept application/json 74 | // @Product application/json 75 | // @param data body models.Tenant true "data" 76 | // @Success 201 {object} models.Tenant 77 | // @Router /admin/api/tenants [post] 78 | // @Security Bearer 79 | func (e *Tenant) Create(*gin.Context) {} 80 | 81 | // Update 更新租户 82 | // @Summary 更新租户 83 | // @Description 更新租户 84 | // @Tags tenant 85 | // @Accept application/json 86 | // @Product application/json 87 | // @param id path string true "id" 88 | // @param data body models.Tenant true "data" 89 | // @Success 200 {object} models.Tenant 90 | // @Router /admin/api/tenants/{id} [put] 91 | // @Security Bearer 92 | func (e *Tenant) Update(*gin.Context) {} 93 | 94 | // Get 获取租户 95 | // @Summary 获取租户 96 | // @Description 获取租户 97 | // @Tags tenant 98 | // @param id path string true "id" 99 | // @Param preloads query []string false "preloads" 100 | // @Success 200 {object} models.Tenant 101 | // @Router /admin/api/tenants/{id} [get] 102 | // @Security Bearer 103 | func (e *Tenant) Get(*gin.Context) {} 104 | 105 | // List 租户列表 106 | // @Summary 租户列表 107 | // @Description 租户列表 108 | // @Tags tenant 109 | // @Accept application/json 110 | // @Product application/json 111 | // @Param current query int false "current" 112 | // @Param pageSize query int false "pageSize" 113 | // @Param id query string false "id" 114 | // @Param name query string false "name" 115 | // @Param status query string false "status" 116 | // @Success 200 {object} response.Page{data=[]models.Tenant} 117 | // @Router /admin/api/tenants [get] 118 | // @Security Bearer 119 | func (e *Tenant) List(*gin.Context) {} 120 | 121 | // Delete 删除租户 122 | // @Summary 删除租户 123 | // @Description 删除租户 124 | // @Tags tenant 125 | // @param id path string true "id" 126 | // @Success 204 127 | // @Router /admin/api/tenants/{id} [delete] 128 | // @Security Bearer 129 | func (e *Tenant) Delete(*gin.Context) {} 130 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | env: 9 | # Use docker.io for Docker Hub if empty 10 | REGISTRY: ghcr.io 11 | # github.repository as / 12 | IMAGE_NAME: ${{ github.repository }} 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | os: [linux, windows, darwin] 20 | arch: [amd64, arm64] 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: Setup golang 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: 1.25 28 | - name: Start Redis 29 | uses: supercharge/redis-github-action@1.8.0 30 | with: 31 | redis-version: 7 32 | - name: Install dependencies 33 | run: make deps 34 | - name: Unit Test 35 | run: make test 36 | - name: Build 37 | run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o admin 38 | - name: Windows 39 | if: matrix.os == 'windows' 40 | run: mv admin admin.exe 41 | - name: Archive artifact 42 | if: matrix.os != 'windows' 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: admin-${{ matrix.os }}-${{ matrix.arch }} 46 | path: | 47 | admin 48 | public 49 | config/*.yml 50 | - name: Archive artifact 51 | if: matrix.os == 'windows' 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: admin-${{ matrix.os }}-${{ matrix.arch }} 55 | path: | 56 | admin.exe 57 | public 58 | config/*.yml 59 | 60 | release: 61 | runs-on: ubuntu-latest 62 | needs: build 63 | steps: 64 | - name: Download artifact 65 | uses: actions/download-artifact@v4 66 | 67 | - name: Get latest release version 68 | id: get_release 69 | run: | 70 | LATEST_RELEASE=$(curl -s https://api.github.com/repos/mss-boot-io/mss-boot-admin-antd/releases/latest) 71 | VERSION=$(echo $LATEST_RELEASE | jq -r '.tag_name') 72 | echo "LATEST_RELEASE_VERSION=${VERSION}" >> $GITHUB_ENV 73 | 74 | - name: Download dist-local.tar.gz 75 | run: | 76 | wget https://github.com/mss-boot-io/mss-boot-admin-antd/releases/download/${{ env.LATEST_RELEASE_VERSION }}/dist-local.tar.gz 77 | tar -zxvf dist-local.tar.gz 78 | cp -r dist admin-linux-amd64/ 79 | cp -r dist admin-linux-arm64/ 80 | cp -r dist admin-darwin-amd64/ 81 | cp -r dist admin-darwin-arm64/ 82 | cp -r dist admin-windows-amd64/ 83 | cp -r dist admin-windows-arm64/ 84 | 85 | - name: Package 86 | run: | 87 | zip -r admin-linux-amd64.zip admin-linux-amd64 88 | zip -r admin-linux-arm64.zip admin-linux-arm64 89 | zip -r admin-darwin-amd64.zip admin-darwin-amd64 90 | zip -r admin-darwin-arm64.zip admin-darwin-arm64 91 | zip -r admin-windows-amd64.zip admin-windows-amd64 92 | zip -r admin-windows-arm64.zip admin-windows-arm64 93 | 94 | - name: Get version 95 | id: get_version 96 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 97 | 98 | - name: Release 99 | uses: softprops/action-gh-release@v1 100 | with: 101 | generate_release_notes: true 102 | files: | 103 | admin-linux-amd64.zip 104 | admin-linux-arm64.zip 105 | admin-darwin-amd64.zip 106 | admin-darwin-arm64.zip 107 | admin-windows-amd64.zip 108 | admin-windows-arm64.zip 109 | prerelease: false 110 | body: | 111 | ## Pull Image 112 | ```shell 113 | docker pull ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.VERSION }} 114 | ``` 115 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | /* 4 | * @Author: lwnmengjing 5 | * @Date: 2023/8/6 08:33:26 6 | * @Last Modified by: lwnmengjing 7 | * @Last Modified time: 2023/8/6 08:33:26 8 | */ 9 | 10 | import ( 11 | "embed" 12 | "log/slog" 13 | "os" 14 | 15 | "github.com/mss-boot-io/mss-boot-admin/center" 16 | "github.com/mss-boot-io/mss-boot/pkg/config" 17 | "github.com/mss-boot-io/mss-boot/pkg/config/gormdb" 18 | "github.com/mss-boot-io/mss-boot/pkg/config/source" 19 | "github.com/mss-boot-io/mss-boot/pkg/config/storage" 20 | "github.com/mss-boot-io/mss-boot/pkg/config/storage/cache" 21 | "github.com/mss-boot-io/mss-boot/pkg/config/storage/queue" 22 | ) 23 | 24 | //go:embed *.yml 25 | var FS embed.FS 26 | 27 | var Cfg = &Config{} 28 | 29 | type Config struct { 30 | Auth Auth `yaml:"auth" json:"auth"` 31 | GRPC config.GRPC `yaml:"grpc" json:"grpc"` 32 | Logger config.Logger `yaml:"logger" json:"logger"` 33 | Server config.Listen `yaml:"server" json:"server"` 34 | Listen *config.Listen `yaml:"listen" json:"listen"` 35 | Database gormdb.Database `yaml:"database" json:"database"` 36 | Application Application `yaml:"application" json:"application"` 37 | //OAuth2 *config.OAuth2 `yaml:"oauth2" json:"oauth2"` 38 | Task Task `yaml:"task" json:"task"` 39 | Pyroscope Pyroscope `yaml:"pyroscope" json:"pyroscope"` 40 | Cache *config.Cache `yaml:"cache" json:"cache"` 41 | Queue *config.Queue `yaml:"queue" json:"queue"` 42 | Locker *config.Locker `yaml:"locker" json:"locker"` 43 | Secret *Secret `yaml:"secret" json:"secret"` 44 | Storage *config.Storage `yaml:"storage" json:"storage"` 45 | Clusters Clusters `yaml:"clusters" json:"clusters"` 46 | } 47 | 48 | type SecretConfig struct { 49 | Secret *Secret `yaml:"secret" json:"secret"` 50 | } 51 | 52 | func (s *SecretConfig) Init() { 53 | if s.Secret != nil { 54 | s.Secret.Init() 55 | } 56 | } 57 | 58 | func (e *Config) Init(opts ...source.Option) { 59 | sc := &SecretConfig{} 60 | opts = append(opts, source.WithPrefixHook(sc)) 61 | 62 | err := config.Init(e, opts...) 63 | if err != nil { 64 | slog.Error("cfg init failed", "err", err) 65 | } 66 | //if e.Logger.Loki != nil && len(e.Application.Labels) > 0 { 67 | // e.Logger.Loki.MergeLabels(e.Application.Labels) 68 | //} 69 | if e.Pyroscope.Enabled && len(e.Application.Labels) > 0 { 70 | e.Pyroscope.MergeTags(e.Application.Labels) 71 | } 72 | e.Logger.Init() 73 | e.Database.Init() 74 | if e.Pyroscope.ApplicationName == "" { 75 | e.Pyroscope.ApplicationName = e.Application.Name 76 | } 77 | e.Pyroscope.Init() 78 | 79 | if e.Cache != nil { 80 | e.Cache.Init(func(c storage.AdapterCache) { 81 | center.SetCache(c) 82 | center.SetVerifyCodeStore(cache.NewVerifyCode(c)) 83 | }, nil) 84 | } 85 | if e.Queue != nil { 86 | e.Queue.Init(func(q storage.AdapterQueue) { 87 | center.SetQueue(q) 88 | w := queue.NewSampleWatcher(q) 89 | err = w.SetUpdateCallback(func(_ string) { 90 | err = gormdb.Enforcer.LoadPolicy() 91 | if err != nil { 92 | slog.Error("enforcer load policy failed", "err", err) 93 | return 94 | } 95 | }) 96 | if err != nil { 97 | slog.Error("casbin set callback failed", slog.Any("err", err)) 98 | os.Exit(-1) 99 | } 100 | err = gormdb.Enforcer.SetWatcher(w) 101 | if err != nil { 102 | slog.Error("casbin set watcher failed", slog.Any("err", err)) 103 | os.Exit(-1) 104 | } 105 | gormdb.Enforcer.EnableAutoNotifyWatcher(true) 106 | }) 107 | } 108 | if e.Locker != nil { 109 | e.Locker.Init(func(l storage.AdapterLocker) { 110 | center.SetLocker(l) 111 | }) 112 | } 113 | if e.Storage != nil { 114 | e.Storage.Init() 115 | } 116 | if len(e.Clusters) > 0 { 117 | e.Clusters.Init() 118 | } 119 | } 120 | 121 | func (e *Config) OnChange() { 122 | e.Logger.Init() 123 | e.Database.Init() 124 | slog.Info("!!! cfg change and reload") 125 | } 126 | -------------------------------------------------------------------------------- /models/field.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/mss-boot-io/mss-boot/pkg/response/actions" 9 | "gorm.io/gorm" 10 | 11 | "github.com/mss-boot-io/mss-boot-admin/pkg" 12 | ) 13 | 14 | /* 15 | * @Author: lwnmengjing 16 | * @Date: 2023/8/21 19:46:33 17 | * @Last Modified by: lwnmengjing 18 | * @Last Modified time: 2023/8/21 19:46:33 19 | */ 20 | 21 | type Fields []*Field 22 | 23 | func (x Fields) Len() int { return len(x) } 24 | func (x Fields) Less(i, j int) bool { return x[i].Sort > x[j].Sort } 25 | func (x Fields) Swap(i, j int) { x[i], x[j] = x[j], x[i] } 26 | 27 | type Field struct { 28 | actions.ModelGorm 29 | ModelID string `gorm:"column:model_id;type:varchar(64);not null;index;comment:模型id" json:"modelID"` 30 | Name string `gorm:"column:name;type:varchar(64);not null;comment:名称" json:"name"` 31 | AssociationsID string `gorm:"column:associations_id;type:varchar(64);comment:关联id" json:"associationsID"` 32 | JsonTag string `gorm:"column:json_tag;type:varchar(64);not null;comment:json标签" json:"jsonTag"` 33 | Label string `gorm:"column:label;type:varchar(64);not null;comment:标签" json:"label"` 34 | Type string `gorm:"column:type;type:varchar(64);not null;comment:数据类型" json:"type"` 35 | Size int `gorm:"column:size;type:int;default:0;comment:大小" json:"size"` 36 | Sort uint `gorm:"column:sort;type:int;default:0;comment:排序" json:"sort"` 37 | PrimaryKey string `gorm:"column:primary_key;type:varchar(100);default:'';comment:主键" json:"primaryKey"` 38 | UniqueIndex string `gorm:"column:unique_index;type:varchar(100);default:'';comment:唯一" json:"unique"` 39 | Index string `gorm:"column:index;type:varchar(100);default:'';comment:索引" json:"index"` 40 | Default string `gorm:"column:default;type:varchar(255);not null;comment:默认值" json:"default"` 41 | Comment string `gorm:"column:comment;type:varchar(255);not null;comment:注释" json:"comment"` 42 | Search string `gorm:"column:search;type:varchar(64);not null;comment:搜索类型" json:"search"` 43 | NotNull bool `gorm:"column:not_null;size:1;not null;comment:是否非空" json:"notNull"` 44 | ValueEnumName string `gorm:"column:value_enum_name;type:varchar(64);not null;comment:枚举值名称" json:"valueEnumName"` 45 | *FieldFrontend `gorm:"column:field_frontend;type:json;comment:前端配置"` 46 | } 47 | 48 | func (*Field) TableName() string { 49 | return "mss_boot_fields" 50 | } 51 | 52 | func (e *Field) BeforeSave(_ *gorm.DB) error { 53 | if e.FieldFrontend != nil { 54 | for i := range e.FieldFrontend.Rules { 55 | if e.FieldFrontend.Rules[i].ID == "" { 56 | e.FieldFrontend.Rules[i].ID = pkg.SimpleID() 57 | } 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | func (e *Field) AfterCreate(tx *gorm.DB) error { 64 | var m Model 65 | err := tx.Where("id = ?", e.ModelID).Preload("Fields").First(&m).Error 66 | if err != nil { 67 | return err 68 | } 69 | if m.GeneratedData { 70 | vm := m.MakeVirtualModel() 71 | if vm == nil { 72 | return fmt.Errorf("make virtual model error") 73 | } 74 | err = vm.Migrate(tx) 75 | if err != nil { 76 | return err 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | type FieldFrontend struct { 83 | HideInTable bool `json:"hideInTable,omitempty"` 84 | HideInForm bool `json:"hideInForm,omitempty"` 85 | HideInDescriptions bool `json:"hideInDescriptions,omitempty"` 86 | Width string `json:"width,omitempty"` 87 | Rules []pkg.BaseRule `json:"rules,omitempty"` 88 | FormComponent string `json:"formComponent,omitempty"` 89 | TableComponent string `json:"tableComponent,omitempty"` 90 | } 91 | 92 | func (f *FieldFrontend) Value() (driver.Value, error) { 93 | if f == nil { 94 | return nil, nil 95 | } 96 | return json.Marshal(f) 97 | } 98 | 99 | func (f *FieldFrontend) Scan(val any) error { 100 | return json.Unmarshal(val.([]uint8), f) 101 | } 102 | -------------------------------------------------------------------------------- /models/post.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/mss-boot-io/mss-boot-admin/pkg" 8 | 9 | "github.com/mss-boot-io/mss-boot/pkg/enum" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | /* 14 | * @Author: lwnmengjing 15 | * @Date: 2024/1/24 18:16:17 16 | * @Last Modified by: lwnmengjing 17 | * @Last Modified time: 2024/1/24 18:16:17 18 | */ 19 | 20 | type DataScope string 21 | 22 | const ( 23 | // DataScopeAll 全部数据权限 24 | DataScopeAll DataScope = "all" 25 | // DataScopeCurrentDept 当前部门数据权限 26 | DataScopeCurrentDept DataScope = "currentDept" 27 | // DataScopeCurrentAndChildrenDept 当前部门及以下数据权限 28 | DataScopeCurrentAndChildrenDept DataScope = "currentAndChildrenDept" 29 | // DataScopeCustomDept 自定义部门 30 | DataScopeCustomDept DataScope = "customDept" 31 | // DataScopeSelf 自己数据权限 32 | DataScopeSelf DataScope = "self" 33 | // DataScopeSelfAndChildren 自己和直属下级 34 | DataScopeSelfAndChildren DataScope = "selfAndChildren" 35 | // DataScopeSelfAndAllChildren 自己和全部下级 36 | DataScopeSelfAndAllChildren DataScope = "selfAndAllChildren" 37 | ) 38 | 39 | type PostList []*Post 40 | 41 | type Post struct { 42 | ModelGormTenant 43 | // ParentID 父级id 44 | ParentID string `json:"parentID,omitempty" gorm:"column:parent_id;comment:父级id;type:varchar(255);default:'';index"` 45 | // Name 岗位名称 46 | Name string `json:"name" gorm:"column:name;comment:岗位名称;type:varchar(255);not null"` 47 | // Code 岗位编码 48 | Code string `json:"code" gorm:"column:code;comment:岗位编码;type:varchar(255);not null"` 49 | // Status 状态 50 | Status enum.Status `json:"status" gorm:"column:status;comment:状态;size:10"` 51 | // Sort 排序 52 | Sort int `json:"sort" gorm:"column:sort;comment:排序;type:int;size:5;defualt:0"` 53 | // DataScope 数据权限 54 | DataScope DataScope `json:"dataScope" gorm:"column:data_scope;comment:数据权限;type:varchar(50)"` 55 | // DeptIDS 部门id 56 | DeptIDS string `json:"-" gorm:"column:dept_ids;comment:部门id;type:varchar(255)"` // 部门id 57 | // DeptIDSArr 部门id数组 58 | DeptIDSArr []string `json:"deptIDS" gorm:"-"` 59 | // Children 子岗位 60 | Children []*Post `json:"children,omitempty" gorm:"foreignKey:ParentID;references:ID" swaggerignore:"true"` 61 | } 62 | 63 | func (e *Post) BeforeSave(_ *gorm.DB) error { 64 | if len(e.DeptIDSArr) > 0 { 65 | e.DeptIDS = strings.Join(e.DeptIDSArr, ",") 66 | } 67 | return nil 68 | } 69 | 70 | func (e *Post) AfterFind(_ *gorm.DB) error { 71 | if e.DeptIDS != "" { 72 | e.DeptIDSArr = strings.Split(e.DeptIDS, ",") 73 | } 74 | return nil 75 | } 76 | 77 | func (*Post) TableName() string { 78 | return "mss_boot_posts" 79 | } 80 | 81 | func (e *Post) GetChildrenID(tx *gorm.DB) []string { 82 | ids := make([]string, 0) 83 | if len(e.Children) == 0 { 84 | tx.Model(&Post{}).Where("parent_id = ?", e.ID).Find(&e.Children) 85 | } 86 | for i := range e.Children { 87 | ids = append(ids, e.Children[i].ID) 88 | } 89 | return ids 90 | } 91 | 92 | func (e *Post) GetAllChildrenID(tx *gorm.DB) []string { 93 | ids := make([]string, 0) 94 | if len(e.Children) == 0 { 95 | tx.Model(&Post{}).Where("parent_id = ?", e.ID).Find(&e.Children) 96 | } 97 | for i := range e.Children { 98 | ids = append(ids, e.Children[i].GetChildrenID(tx)...) 99 | } 100 | return ids 101 | } 102 | 103 | func (x PostList) Len() int { return len(x) } 104 | func (x PostList) Less(i, j int) bool { return x[i].Sort > x[j].Sort } 105 | func (x PostList) Swap(i, j int) { x[i], x[j] = x[j], x[i] } 106 | 107 | func (e *Post) GetIndex() string { 108 | return e.ID 109 | } 110 | 111 | func (e *Post) GetParentID() string { 112 | return e.ParentID 113 | } 114 | 115 | func (e *Post) SortChildren() { 116 | if len(e.Children) == 0 { 117 | return 118 | } 119 | sort.Sort(PostList(e.Children)) 120 | for i := range e.Children { 121 | e.Children[i].SortChildren() 122 | } 123 | } 124 | 125 | func (e *Post) AddChildren(children []pkg.TreeImp) { 126 | if e.Children == nil { 127 | e.Children = make([]*Post, 0) 128 | } 129 | for i := range children { 130 | e.Children = append(e.Children, children[i].(*Post)) 131 | } 132 | } 133 | --------------------------------------------------------------------------------