├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── CHANGELOG ├── Dockerfile ├── LICENSE ├── README.md ├── README_zh.md ├── cmd ├── migration │ ├── main.go │ └── wire │ │ ├── wire.go │ │ └── wire_gen.go ├── server │ ├── main.go │ └── wire │ │ ├── wire.go │ │ └── wire_gen.go └── task │ ├── main.go │ └── wire │ ├── wire.go │ └── wire_gen.go ├── config ├── local.yml └── prod.yml ├── database └── helloadmin.sql ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── images ├── dashboard.png ├── dept.png ├── login.png ├── logo.png └── role.png ├── internal ├── api │ └── response.go ├── department │ ├── api_types.go │ ├── handler.go │ ├── model.go │ ├── repository.go │ └── service.go ├── ecode │ ├── errcode_string.go │ └── error.go ├── login_record │ ├── api_types.go │ ├── handler.go │ ├── model.go │ ├── respository.go │ └── service.go ├── menu │ ├── api_types.go │ ├── const.go │ ├── handler.go │ ├── model.go │ ├── repository.go │ └── service.go ├── middleware │ ├── cors.go │ ├── jwt.go │ ├── log.go │ └── sign.go ├── operation_record │ └── model.go ├── repository │ └── repository.go ├── role │ ├── api_types.go │ ├── handler.go │ ├── model.go │ ├── repository.go │ └── service.go ├── server │ ├── http.go │ ├── job.go │ ├── migration.go │ └── task.go └── user │ ├── api_types.go │ ├── handler.go │ ├── model.go │ ├── respository.go │ └── service.go ├── pkg ├── app │ └── app.go ├── config │ └── config.go ├── helper │ ├── convert │ │ └── convert.go │ ├── generate │ │ └── generate.go │ ├── md5 │ │ └── md5.go │ ├── sid │ │ └── sid.go │ └── uuid │ │ └── uuid.go ├── jwt │ └── jwt.go ├── log │ └── log.go └── server │ ├── grpc │ └── grpc.go │ ├── http │ └── http.go │ └── server.go ├── scripts └── README.md ├── test ├── mocks │ ├── repository │ │ ├── repository.go │ │ └── user.go │ └── service │ │ ├── role.go │ │ └── user.go └── server │ ├── handler │ ├── role_test.go │ └── user_test.go │ ├── repository │ └── user_test.go │ └── service │ └── user_test.go └── web └── index.html /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: hello-admin 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - '*' 8 | pull_request: 9 | branches: 10 | - main 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v4 18 | - 19 | name: Set up QEMU 20 | uses: docker/setup-qemu-action@v3 21 | - 22 | name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v3 24 | - 25 | name: Login to Docker Hub 26 | uses: docker/login-action@v3 27 | with: 28 | username: ${{ secrets.DOCKER_USERNAME }} 29 | password: ${{ secrets.DOCKER_TOKEN }} 30 | - 31 | name: Build and push 32 | uses: docker/build-push-action@v5 33 | with: 34 | context: . 35 | push: true 36 | tags: itbing/helloadmin:latest 37 | deploy: 38 | runs-on: ubuntu-latest 39 | needs: build 40 | steps: 41 | - name: Deploy API 42 | uses: appleboy/ssh-action@master 43 | with: 44 | host: ${{ secrets.DEV_SSH_HOST }} 45 | username: ${{ secrets.DEV_SSH_USERNAME }} 46 | key: ${{ secrets.DEV_SSH_KEY }} 47 | port: ${{ secrets.DEV_SSH_PORT }} 48 | script_stop: true 49 | script: | 50 | cd golang/helloadmin && docker-compose down --rmi all && docker-compose up -d -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | .env 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heliosker/helloadmin/e37641503ebdb7de3584fcbeea89914c9adb8e05/CHANGELOG -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 AS builder 2 | 3 | WORKDIR /go/src 4 | 5 | COPY .. . 6 | 7 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server cmd/server/main.go 8 | 9 | FROM alpine:latest as prod 10 | 11 | RUN apk --no-cache add ca-certificates 12 | RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 13 | && echo "Asia/Shanghai" > /etc/timezone \ 14 | && apk del tzdata 15 | 16 | 17 | WORKDIR /root/ 18 | 19 | COPY --from=builder /go/src/server . 20 | 21 | EXPOSE 8080 22 | 23 | ENTRYPOINT [ "./server" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 HelloAdmin 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

HelloAdmin Logo

3 | 4 | # HelloAdmin 5 | 6 | HelloAdmin is a front-end and back-end separation management system based on Gin + Ant Design Vue UI. The front-end uses Ant Design Vue UI, and the back-end uses the Gin framework. 7 | 8 | 中文文档: [README_zh.md](/README_zh.md) 9 | 10 | ## Built-in Features 11 | 12 | - [x] JWT Login 13 | - [x] Role Management 14 | - [x] Menu Management 15 | - [x] Department Management 16 | - [x] Operation Log 17 | 18 | Experience it at: [http://demo.helloadmin.cn](http://demo.helloadmin.cn) 19 | 20 | ## Features 21 | 22 | * Gin: https://github.com/gin-gonic/gin 23 | * Gorm: https://github.com/go-gorm/gorm 24 | * Wire: https://github.com/google/wire 25 | * Viper: https://github.com/spf13/viper 26 | * Zap: https://github.com/uber-go/zap 27 | * Golang-jwt: https://github.com/golang-jwt/jwt 28 | * Go-redis: https://github.com/go-redis/redis 29 | * Testify: https://github.com/stretchr/testify 30 | * Sonyflake: https://github.com/sony/sonyflake 31 | * Gocron: https://github.com/go-co-op/gocron 32 | * Go-sqlmock: https://github.com/DATA-DOG/go-sqlmock 33 | * Gomock: https://github.com/golang/mock 34 | * Swaggo: https://github.com/swaggo/swag 35 | 36 | ## Screenshots 37 | 38 | ![Login](https://raw.githubusercontent.com/heliosker/helloadmin/main/images/login.png) 39 | 40 | ![Dashboard](https://raw.githubusercontent.com/heliosker/helloadmin/main/images/dashboard.png) 41 | 42 | ![dept](https://raw.githubusercontent.com/heliosker/helloadmin/main/images/dept.png) 43 | 44 | ![role](https://raw.githubusercontent.com/heliosker/helloadmin/main/images/role.png) 45 | 46 | ## Frontend Repository 47 | 48 | Link: [Helloadmin-ui](https://github.com/susie721/helloadmin-vue) 49 | 50 | ## License 51 | 52 | HelloAdmin is released under the MIT License. For more information, please see the [LICENSE](LICENSE) file. -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 |

HelloAdmin Logo

2 | 3 | # HelloAdmin 4 | 5 | HelloAdmin 是一个基于 Gin + Ant Design Vue UI 的前后端分离管理系统,前端使用 Ant Design Vue UI,后端使用 Gin 框架。 6 | 7 | ## 内置功能 8 | 9 | - [x] JWT 登录 10 | - [x] 角色管理 11 | - [x] 菜单管理 12 | - [x] 部门管理 13 | - [x] 操作日志 14 | 15 | 体验地址: [http://demo.helloadmin.cn](http://demo.helloadmin.cn) 16 | 17 | ## 特性 18 | 19 | * Gin: https://github.com/gin-gonic/gin 20 | * Gorm: https://github.com/go-gorm/gorm 21 | * Wire: https://github.com/google/wire 22 | * Viper: https://github.com/spf13/viper 23 | * Zap: https://github.com/uber-go/zap 24 | * Golang-jwt: https://github.com/golang-jwt/jwt 25 | * Go-redis: https://github.com/go-redis/redis 26 | * Testify: https://github.com/stretchr/testify 27 | * Sonyflake: https://github.com/sony/sonyflake 28 | * Gocron: https://github.com/go-co-op/gocron 29 | * Go-sqlmock: https://github.com/DATA-DOG/go-sqlmock 30 | * Gomock: https://github.com/golang/mock 31 | * Swaggo: https://github.com/swaggo/swag 32 | 33 | ## 效果图 34 | 35 | ![Login](https://raw.githubusercontent.com/heliosker/helloadmin/main/images/login.png) 36 | 37 | ![Dashboard](https://raw.githubusercontent.com/heliosker/helloadmin/main/images/dashboard.png) 38 | 39 | ![dept](https://raw.githubusercontent.com/heliosker/helloadmin/main/images/dept.png) 40 | 41 | ![role](https://raw.githubusercontent.com/heliosker/helloadmin/main/images/role.png) 42 | 43 | ## 前端仓库 44 | 45 | Link:[Helloadmin-ui](https://github.com/susie721/helloadmin-vue) 46 | 47 | ## 许可证 48 | 49 | HelloAdmin 是根据MIT许可证发布的。有关更多信息,请参见[LICENSE](LICENSE)文件。 50 | -------------------------------------------------------------------------------- /cmd/migration/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | 7 | "helloadmin/cmd/migration/wire" 8 | "helloadmin/pkg/config" 9 | "helloadmin/pkg/log" 10 | ) 11 | 12 | func main() { 13 | envConf := flag.String("conf", "config/local.yml", "config path, eg: -conf ./config/local.yml") 14 | flag.Parse() 15 | conf := config.NewConfig(*envConf) 16 | 17 | logger := log.NewLog(conf) 18 | 19 | app, cleanup, err := wire.NewWire(conf, logger) 20 | defer cleanup() 21 | if err != nil { 22 | panic(err) 23 | } 24 | if err = app.Run(context.Background()); err != nil { 25 | panic(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cmd/migration/wire/wire.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | // +build wireinject 3 | 4 | package wire 5 | 6 | import ( 7 | "github.com/google/wire" 8 | "github.com/spf13/viper" 9 | "helloadmin/internal/repository" 10 | "helloadmin/internal/server" 11 | "helloadmin/internal/user" 12 | "helloadmin/pkg/app" 13 | "helloadmin/pkg/log" 14 | ) 15 | 16 | var repositorySet = wire.NewSet( 17 | repository.NewDB, 18 | repository.NewRedis, 19 | repository.NewRepository, 20 | user.NewRepository, 21 | ) 22 | 23 | // build App 24 | func newApp(migrate *server.Migrate) *app.App { 25 | return app.NewApp( 26 | app.WithServer(migrate), 27 | app.WithName("demo-migrate"), 28 | ) 29 | } 30 | 31 | func NewWire(*viper.Viper, *log.Logger) (*app.App, func(), error) { 32 | panic(wire.Build( 33 | repositorySet, 34 | server.NewMigrate, 35 | newApp, 36 | )) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/migration/wire/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate go run github.com/google/wire/cmd/wire 4 | //go:build !wireinject 5 | // +build !wireinject 6 | 7 | package wire 8 | 9 | import ( 10 | "github.com/google/wire" 11 | "github.com/spf13/viper" 12 | "helloadmin/internal/repository" 13 | "helloadmin/internal/server" 14 | "helloadmin/internal/user" 15 | "helloadmin/pkg/app" 16 | "helloadmin/pkg/log" 17 | ) 18 | 19 | // Injectors from wire.go: 20 | 21 | func NewWire(viperViper *viper.Viper, logger *log.Logger) (*app.App, func(), error) { 22 | db := repository.NewDB(viperViper, logger) 23 | migrate := server.NewMigrate(db, logger) 24 | appApp := newApp(migrate) 25 | return appApp, func() { 26 | }, nil 27 | } 28 | 29 | // wire.go: 30 | 31 | var repositorySet = wire.NewSet(repository.NewDB, repository.NewRedis, repository.NewRepository, user.NewRepository) 32 | 33 | // build App 34 | func newApp(migrate *server.Migrate) *app.App { 35 | return app.NewApp(app.WithServer(migrate), app.WithName("demo-migrate")) 36 | } 37 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "go.uber.org/zap" 9 | "helloadmin/cmd/server/wire" 10 | "helloadmin/pkg/config" 11 | "helloadmin/pkg/log" 12 | ) 13 | 14 | // @title HelloAdmin API 15 | // @version 1.0.0 16 | // @description This is a sample HelloAdmin API. 17 | // @termsOfService http://swagger.io/terms/ 18 | // @contact.name API Support 19 | // @contact.url http://www.swagger.io/support 20 | // @contact.email support@swagger.io 21 | // @license.name Apache 2.0 22 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 23 | // @host localhost:8080 24 | // @securityDefinitions.apiKey Bearer 25 | // @in header 26 | // @name Authorization 27 | // @externalDocs.description OpenAPI 28 | // @externalDocs.url https://swagger.io/resources/open-api/ 29 | func main() { 30 | envConf := flag.String("conf", "config/local.yml", "config path, eg: -conf ./config/local.yml") 31 | flag.Parse() 32 | conf := config.NewConfig(*envConf) 33 | 34 | logger := log.NewLog(conf) 35 | 36 | app, cleanup, err := wire.NewWire(conf, logger) 37 | defer cleanup() 38 | if err != nil { 39 | panic(err) 40 | } 41 | logger.Info("server start", zap.String("host", "http://127.0.0.1:"+conf.GetString("http.port"))) 42 | logger.Info("docs addr", zap.String("addr", fmt.Sprintf("http://127.0.0.1:%d/swagger/index.html", conf.GetInt("http.port")))) 43 | if err = app.Run(context.Background()); err != nil { 44 | panic(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmd/server/wire/wire.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | // +build wireinject 3 | 4 | package wire 5 | 6 | import ( 7 | "github.com/google/wire" 8 | "github.com/spf13/viper" 9 | "helloadmin/internal/department" 10 | "helloadmin/internal/login_record" 11 | "helloadmin/internal/menu" 12 | "helloadmin/internal/repository" 13 | "helloadmin/internal/role" 14 | "helloadmin/internal/server" 15 | "helloadmin/internal/user" 16 | "helloadmin/pkg/app" 17 | "helloadmin/pkg/helper/sid" 18 | "helloadmin/pkg/jwt" 19 | "helloadmin/pkg/log" 20 | "helloadmin/pkg/server/http" 21 | ) 22 | 23 | var repositorySet = wire.NewSet( 24 | repository.NewDB, 25 | repository.NewRedis, 26 | repository.NewRepository, 27 | repository.NewTransaction, 28 | role.NewRepository, 29 | menu.NewRepository, 30 | department.NewRepository, 31 | login_record.NewRepository, 32 | user.NewRepository, 33 | ) 34 | 35 | var serviceSet = wire.NewSet( 36 | role.NewService, 37 | menu.NewService, 38 | department.NewService, 39 | login_record.NewService, 40 | user.NewService, 41 | ) 42 | 43 | var handlerSet = wire.NewSet( 44 | role.NewHandler, 45 | menu.NewHandler, 46 | department.NewHandler, 47 | login_record.NewHandler, 48 | user.NewHandler, 49 | ) 50 | 51 | var serverSet = wire.NewSet( 52 | server.NewHTTPServer, 53 | server.NewJob, 54 | server.NewTask, 55 | ) 56 | 57 | // build App 58 | func newApp(httpServer *http.Server, job *server.Job) *app.App { 59 | return app.NewApp( 60 | app.WithServer(httpServer, job), 61 | app.WithName("hello-admin-server"), 62 | ) 63 | } 64 | 65 | func NewWire(*viper.Viper, *log.Logger) (*app.App, func(), error) { 66 | panic(wire.Build( 67 | repositorySet, 68 | serviceSet, 69 | handlerSet, 70 | serverSet, 71 | sid.NewSid, 72 | jwt.NewJwt, 73 | newApp, 74 | )) 75 | } 76 | -------------------------------------------------------------------------------- /cmd/server/wire/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate go run github.com/google/wire/cmd/wire 4 | //go:build !wireinject 5 | // +build !wireinject 6 | 7 | package wire 8 | 9 | import ( 10 | "github.com/google/wire" 11 | "github.com/spf13/viper" 12 | "helloadmin/internal/department" 13 | "helloadmin/internal/login_record" 14 | "helloadmin/internal/menu" 15 | "helloadmin/internal/repository" 16 | "helloadmin/internal/role" 17 | "helloadmin/internal/server" 18 | "helloadmin/internal/user" 19 | "helloadmin/pkg/app" 20 | "helloadmin/pkg/helper/sid" 21 | "helloadmin/pkg/jwt" 22 | "helloadmin/pkg/log" 23 | "helloadmin/pkg/server/http" 24 | ) 25 | 26 | // Injectors from wire.go: 27 | 28 | func NewWire(viperViper *viper.Viper, logger *log.Logger) (*app.App, func(), error) { 29 | jwtJWT := jwt.NewJwt(viperViper) 30 | sidSid := sid.NewSid() 31 | db := repository.NewDB(viperViper, logger) 32 | client := repository.NewRedis(viperViper) 33 | repositoryRepository := repository.NewRepository(db, client, logger) 34 | userRepository := user.NewRepository(repositoryRepository) 35 | service := user.NewService(sidSid, jwtJWT, userRepository) 36 | login_recordRepository := login_record.NewRepository(repositoryRepository) 37 | login_recordService := login_record.NewService(login_recordRepository) 38 | departmentRepository := department.NewRepository(repositoryRepository) 39 | departmentService := department.NewService(departmentRepository) 40 | roleRepository := role.NewRepository(repositoryRepository) 41 | roleService := role.NewService(roleRepository) 42 | handler := user.NewHandler(logger, service, login_recordService, departmentService, roleService) 43 | roleHandler := role.NewHandler(logger, roleService) 44 | menuRepository := menu.NewRepository(repositoryRepository) 45 | menuService := menu.NewService(menuRepository) 46 | menuHandler := menu.NewHandler(logger, menuService) 47 | departmentHandler := department.NewHandler(logger, departmentService) 48 | login_recordHandler := login_record.NewHandler(logger, login_recordService) 49 | httpServer := server.NewHTTPServer(logger, viperViper, jwtJWT, handler, roleHandler, menuHandler, departmentHandler, login_recordHandler) 50 | job := server.NewJob(logger) 51 | appApp := newApp(httpServer, job) 52 | return appApp, func() { 53 | }, nil 54 | } 55 | 56 | // wire.go: 57 | 58 | var repositorySet = wire.NewSet(repository.NewDB, repository.NewRedis, repository.NewRepository, repository.NewTransaction, role.NewRepository, menu.NewRepository, department.NewRepository, login_record.NewRepository, user.NewRepository) 59 | 60 | var serviceSet = wire.NewSet(role.NewService, menu.NewService, department.NewService, login_record.NewService, user.NewService) 61 | 62 | var handlerSet = wire.NewSet(role.NewHandler, menu.NewHandler, department.NewHandler, login_record.NewHandler, user.NewHandler) 63 | 64 | var serverSet = wire.NewSet(server.NewHTTPServer, server.NewJob, server.NewTask) 65 | 66 | // build App 67 | func newApp(httpServer *http.Server, job *server.Job) *app.App { 68 | return app.NewApp(app.WithServer(httpServer, job), app.WithName("hello-admin-server")) 69 | } 70 | -------------------------------------------------------------------------------- /cmd/task/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | 7 | "helloadmin/cmd/task/wire" 8 | "helloadmin/pkg/config" 9 | "helloadmin/pkg/log" 10 | ) 11 | 12 | func main() { 13 | envConf := flag.String("conf", "config/local.yml", "config path, eg: -conf ./config/local.yml") 14 | flag.Parse() 15 | conf := config.NewConfig(*envConf) 16 | 17 | logger := log.NewLog(conf) 18 | logger.Info("start task") 19 | app, cleanup, err := wire.NewWire(conf, logger) 20 | defer cleanup() 21 | if err != nil { 22 | panic(err) 23 | } 24 | if err = app.Run(context.Background()); err != nil { 25 | panic(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cmd/task/wire/wire.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | // +build wireinject 3 | 4 | package wire 5 | 6 | import ( 7 | "github.com/google/wire" 8 | "github.com/spf13/viper" 9 | "helloadmin/internal/server" 10 | "helloadmin/pkg/app" 11 | "helloadmin/pkg/log" 12 | ) 13 | 14 | var taskSet = wire.NewSet(server.NewTask) 15 | 16 | // build App 17 | func newApp(task *server.Task) *app.App { 18 | return app.NewApp( 19 | app.WithServer(task), 20 | app.WithName("demo-task"), 21 | ) 22 | } 23 | 24 | func NewWire(*viper.Viper, *log.Logger) (*app.App, func(), error) { 25 | panic(wire.Build( 26 | taskSet, 27 | newApp, 28 | )) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/task/wire/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate go run github.com/google/wire/cmd/wire 4 | //go:build !wireinject 5 | // +build !wireinject 6 | 7 | package wire 8 | 9 | import ( 10 | "github.com/google/wire" 11 | "github.com/spf13/viper" 12 | "helloadmin/internal/server" 13 | "helloadmin/pkg/app" 14 | "helloadmin/pkg/log" 15 | ) 16 | 17 | // Injectors from wire.go: 18 | 19 | func NewWire(viperViper *viper.Viper, logger *log.Logger) (*app.App, func(), error) { 20 | task := server.NewTask(logger) 21 | appApp := newApp(task) 22 | return appApp, func() { 23 | }, nil 24 | } 25 | 26 | // wire.go: 27 | 28 | var taskSet = wire.NewSet(server.NewTask) 29 | 30 | // build App 31 | func newApp(task *server.Task) *app.App { 32 | return app.NewApp(app.WithServer(task), app.WithName("demo-task")) 33 | } 34 | -------------------------------------------------------------------------------- /config/local.yml: -------------------------------------------------------------------------------- 1 | app: 2 | env: local 3 | name: helloAdmin 4 | http: 5 | host: 0.0.0.0 6 | port: 8080 7 | security: 8 | api_sign: 9 | app_key: 123456 10 | app_security: 123456 11 | jwt: 12 | key: QQYnRFerJTSEcrfB89fw8prOaObmrch8 13 | data: 14 | mysql: 15 | user: root:123456@tcp(47.103.204.136:3306)/helloadmin?charset=utf8mb4&parseTime=True&loc=Local 16 | redis: 17 | addr: 47.103.204.136:6379 18 | password: "6Pl9xaBKNmUm9JWe2i7VubGxfavhdj8y" 19 | db: 0 20 | read_timeout: 0.2s 21 | write_timeout: 0.2s 22 | 23 | log: 24 | log_level: debug 25 | encoding: console # json or console 26 | log_file_name: "./storage/logs/server.log" 27 | max_backups: 30 28 | max_age: 7 29 | max_size: 1024 30 | compress: true -------------------------------------------------------------------------------- /config/prod.yml: -------------------------------------------------------------------------------- 1 | app: 2 | env: local 3 | name: helloAdmin 4 | http: 5 | host: 0.0.0.0 6 | port: 8000 7 | security: 8 | api_sign: 9 | app_key: 123456 10 | app_security: 123456 11 | jwt: 12 | key: QQYnRFerJTSEcrfB89fw8prOaObmrch8 13 | data: 14 | mysql: 15 | user: root:123456@tcp(127.0.0.1:3380)/user?charset=utf8mb4&parseTime=True&loc=Local 16 | redis: 17 | addr: 127.0.0.1:6350 18 | password: "" 19 | db: 0 20 | read_timeout: 0.2s 21 | write_timeout: 0.2s 22 | 23 | log: 24 | log_level: info 25 | encoding: json # json or console 26 | log_file_name: "./storage/logs/server.log" 27 | max_backups: 30 28 | max_age: 7 29 | max_size: 1024 30 | compress: true -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | api.Response: 3 | properties: 4 | code: 5 | type: integer 6 | data: {} 7 | message: 8 | type: string 9 | type: object 10 | department.CreateRequest: 11 | properties: 12 | leader: 13 | description: 部门负责人 14 | type: string 15 | name: 16 | description: 部门名称 17 | type: string 18 | parentId: 19 | description: 上级部门 20 | type: integer 21 | sort: 22 | description: 排序值,值越大,显示顺序越靠前 23 | type: integer 24 | required: 25 | - name 26 | - sort 27 | type: object 28 | department.Response: 29 | properties: 30 | items: 31 | items: 32 | $ref: '#/definitions/department.ResponseItem' 33 | type: array 34 | type: object 35 | department.ResponseItem: 36 | properties: 37 | children: 38 | items: 39 | $ref: '#/definitions/department.ResponseItem' 40 | type: array 41 | createdAt: 42 | type: string 43 | id: 44 | type: integer 45 | leader: 46 | type: string 47 | name: 48 | type: string 49 | parentId: 50 | type: integer 51 | sort: 52 | type: integer 53 | updateAt: 54 | type: string 55 | type: object 56 | department.UpdateRequest: 57 | properties: 58 | leader: 59 | type: string 60 | name: 61 | type: string 62 | parentId: 63 | type: integer 64 | sort: 65 | type: integer 66 | type: object 67 | menu.CreateRequest: 68 | properties: 69 | component: 70 | description: 组件路径 71 | type: string 72 | icon: 73 | description: 菜单图标 74 | maxLength: 128 75 | type: string 76 | name: 77 | description: 菜单名称 78 | maxLength: 128 79 | type: string 80 | parentId: 81 | description: 上级菜单ID 82 | minimum: 0 83 | type: integer 84 | path: 85 | description: 菜单路径 86 | maxLength: 255 87 | type: string 88 | sort: 89 | description: 排序值,值越大越靠前 90 | type: integer 91 | title: 92 | description: 菜单标题 93 | maxLength: 128 94 | type: string 95 | type: 96 | description: 菜单类型 目录D 菜单M 按钮B 97 | type: string 98 | visible: 99 | description: 是否可见,Y可见 N不可见 100 | type: string 101 | required: 102 | - title 103 | - type 104 | - visible 105 | type: object 106 | menu.Option: 107 | properties: 108 | label: 109 | type: string 110 | value: 111 | type: integer 112 | type: object 113 | menu.Response: 114 | properties: 115 | items: 116 | items: 117 | $ref: '#/definitions/menu.ResponseItem' 118 | type: array 119 | type: object 120 | menu.ResponseItem: 121 | properties: 122 | children: 123 | items: 124 | $ref: '#/definitions/menu.ResponseItem' 125 | type: array 126 | component: 127 | type: string 128 | createdAt: 129 | type: string 130 | icon: 131 | type: string 132 | id: 133 | type: integer 134 | name: 135 | type: string 136 | parentId: 137 | type: integer 138 | path: 139 | type: string 140 | sort: 141 | type: integer 142 | title: 143 | type: string 144 | type: 145 | type: string 146 | updatedAt: 147 | type: string 148 | visible: 149 | type: string 150 | type: object 151 | menu.UpdateRequest: 152 | properties: 153 | component: 154 | description: 组件路径 155 | type: string 156 | icon: 157 | description: 菜单图标 158 | type: string 159 | name: 160 | description: 菜单名称 161 | type: string 162 | parentId: 163 | description: 上级菜单ID 164 | type: integer 165 | path: 166 | description: 菜单路径 167 | type: string 168 | sort: 169 | description: 排序值,值越大越靠前 170 | type: integer 171 | title: 172 | description: 菜单标题 173 | type: string 174 | type: 175 | description: 菜单类型 目录D 菜单M 按钮B 176 | type: string 177 | visible: 178 | description: 是否可见,Y可见 N不可见 179 | type: string 180 | type: object 181 | role.CreateRequest: 182 | properties: 183 | describe: 184 | description: 角色描述 185 | example: this is describe 186 | maxLength: 255 187 | type: string 188 | name: 189 | description: 角色名称 190 | example: test 191 | maxLength: 50 192 | minLength: 1 193 | type: string 194 | required: 195 | - name 196 | type: object 197 | role.MenuRequest: 198 | properties: 199 | menuId: 200 | description: 菜单ID 201 | items: 202 | type: integer 203 | type: array 204 | uniqueItems: true 205 | required: 206 | - menuId 207 | type: object 208 | role.UpdateRequest: 209 | properties: 210 | describe: 211 | example: this is describe 212 | maxLength: 255 213 | type: string 214 | name: 215 | example: test 216 | maxLength: 50 217 | minLength: 1 218 | type: string 219 | required: 220 | - name 221 | type: object 222 | user.LoginRequest: 223 | properties: 224 | email: 225 | description: 邮箱 226 | example: admin@helloadmin.com 227 | type: string 228 | password: 229 | description: 密码 230 | example: "123456" 231 | type: string 232 | required: 233 | - email 234 | - password 235 | type: object 236 | user.LoginResponse: 237 | properties: 238 | accessToken: 239 | description: 访问令牌 240 | type: string 241 | expiresAt: 242 | description: 过期日期 243 | type: string 244 | tokenType: 245 | description: 令牌类型 246 | type: string 247 | type: object 248 | user.ProfileData: 249 | properties: 250 | createdAt: 251 | example: "2023-12-27 19:01:00" 252 | type: string 253 | department: 254 | properties: 255 | id: 256 | type: integer 257 | name: 258 | type: string 259 | type: object 260 | deptId: 261 | description: 员工部门ID 262 | example: 1 263 | type: integer 264 | email: 265 | description: 员工邮箱 266 | example: admin@helloadmin.com 267 | type: string 268 | id: 269 | description: 员工ID 270 | example: 1 271 | type: integer 272 | nickname: 273 | example: Hi admin 274 | type: string 275 | role: 276 | properties: 277 | id: 278 | type: integer 279 | name: 280 | type: string 281 | type: object 282 | roleId: 283 | description: 员工角色ID 284 | example: 1 285 | type: integer 286 | updatedAt: 287 | example: "2023-12-27 19:01:00" 288 | type: string 289 | userId: 290 | description: 员工编码 291 | example: "1" 292 | type: string 293 | type: object 294 | user.RegisterRequest: 295 | properties: 296 | deptId: 297 | description: 部门ID 298 | example: 1 299 | type: integer 300 | email: 301 | description: 邮箱 302 | example: admin@helloadmin.com 303 | type: string 304 | nickname: 305 | description: 员工名 306 | example: admin 307 | maxLength: 50 308 | type: string 309 | password: 310 | description: 密码 311 | example: "123456" 312 | type: string 313 | roleId: 314 | description: 角色ID 315 | example: 1 316 | type: integer 317 | required: 318 | - email 319 | - nickname 320 | - password 321 | type: object 322 | user.UpdateRequest: 323 | properties: 324 | deptId: 325 | description: 部门ID 326 | example: 1 327 | type: integer 328 | email: 329 | example: admin@helloadmin.com 330 | type: string 331 | nickname: 332 | example: admin 333 | type: string 334 | roleId: 335 | description: 角色ID 336 | example: 1 337 | type: integer 338 | required: 339 | - email 340 | type: object 341 | host: localhost:8080 342 | info: 343 | contact: 344 | email: support@swagger.io 345 | name: API Support 346 | url: http://www.swagger.io/support 347 | description: This is a sample HelloAdmin API. 348 | license: 349 | name: Apache 2.0 350 | url: http://www.apache.org/licenses/LICENSE-2.0.html 351 | termsOfService: http://swagger.io/terms/ 352 | title: HelloAdmin API 353 | version: 1.0.0 354 | paths: 355 | /department: 356 | get: 357 | consumes: 358 | - application/json 359 | description: 查询部门列表 360 | parameters: 361 | - description: 部门名称 362 | in: query 363 | maxLength: 50 364 | name: name 365 | type: string 366 | produces: 367 | - application/json 368 | responses: 369 | "200": 370 | description: OK 371 | schema: 372 | $ref: '#/definitions/department.Response' 373 | security: 374 | - Bearer: [] 375 | summary: 部门列表 376 | tags: 377 | - 部门模块 378 | post: 379 | consumes: 380 | - application/json 381 | description: 创建部门 382 | parameters: 383 | - description: params 384 | in: body 385 | name: request 386 | required: true 387 | schema: 388 | $ref: '#/definitions/department.CreateRequest' 389 | produces: 390 | - application/json 391 | responses: 392 | "200": 393 | description: OK 394 | schema: 395 | $ref: '#/definitions/department.Response' 396 | security: 397 | - Bearer: [] 398 | summary: 创建部门 399 | tags: 400 | - 部门模块 401 | /department/{id}: 402 | delete: 403 | consumes: 404 | - application/json 405 | description: 删除单个部门 406 | parameters: 407 | - description: 部门ID 408 | in: path 409 | name: id 410 | required: true 411 | type: integer 412 | produces: 413 | - application/json 414 | responses: 415 | "200": 416 | description: OK 417 | schema: 418 | $ref: '#/definitions/department.Response' 419 | security: 420 | - Bearer: [] 421 | summary: 删除部门 422 | tags: 423 | - 部门模块 424 | get: 425 | consumes: 426 | - application/json 427 | description: 查询单个部门信息 428 | parameters: 429 | - description: 部门ID 430 | in: path 431 | name: id 432 | required: true 433 | type: integer 434 | produces: 435 | - application/json 436 | responses: 437 | "200": 438 | description: OK 439 | schema: 440 | $ref: '#/definitions/department.Response' 441 | security: 442 | - Bearer: [] 443 | summary: 查询部门 444 | tags: 445 | - 部门模块 446 | put: 447 | consumes: 448 | - application/json 449 | description: 修改单个部门信息 450 | parameters: 451 | - description: 部门ID 452 | in: path 453 | name: id 454 | required: true 455 | type: integer 456 | - description: params 457 | in: body 458 | name: request 459 | required: true 460 | schema: 461 | $ref: '#/definitions/department.UpdateRequest' 462 | produces: 463 | - application/json 464 | responses: 465 | "200": 466 | description: OK 467 | schema: 468 | $ref: '#/definitions/department.Response' 469 | security: 470 | - Bearer: [] 471 | summary: 修改部门 472 | tags: 473 | - 部门模块 474 | /login: 475 | post: 476 | consumes: 477 | - application/json 478 | parameters: 479 | - description: params 480 | in: body 481 | name: request 482 | required: true 483 | schema: 484 | $ref: '#/definitions/user.LoginRequest' 485 | produces: 486 | - application/json 487 | responses: 488 | "200": 489 | description: OK 490 | schema: 491 | $ref: '#/definitions/user.LoginResponse' 492 | summary: 员工登录 493 | tags: 494 | - 员工模块 495 | /menu: 496 | get: 497 | consumes: 498 | - application/json 499 | description: 查询菜单列表 500 | parameters: 501 | - description: 菜单名称 502 | in: query 503 | name: name 504 | type: string 505 | - description: 是否可见,Y可见 N不可见 506 | in: query 507 | name: visible 508 | type: string 509 | produces: 510 | - application/json 511 | responses: 512 | "200": 513 | description: OK 514 | schema: 515 | $ref: '#/definitions/menu.Response' 516 | security: 517 | - Bearer: [] 518 | summary: 菜单列表 519 | tags: 520 | - 菜单模块 521 | post: 522 | consumes: 523 | - application/json 524 | description: 创建菜单 525 | parameters: 526 | - description: params 527 | in: body 528 | name: request 529 | required: true 530 | schema: 531 | $ref: '#/definitions/menu.CreateRequest' 532 | produces: 533 | - application/json 534 | responses: 535 | "200": 536 | description: OK 537 | schema: 538 | $ref: '#/definitions/api.Response' 539 | security: 540 | - Bearer: [] 541 | summary: 创建菜单 542 | tags: 543 | - 菜单模块 544 | /menu/{id}: 545 | delete: 546 | consumes: 547 | - application/json 548 | description: 删除单个菜单 549 | parameters: 550 | - description: 菜单ID 551 | in: path 552 | name: id 553 | required: true 554 | type: integer 555 | produces: 556 | - application/json 557 | responses: 558 | "200": 559 | description: OK 560 | schema: 561 | $ref: '#/definitions/api.Response' 562 | security: 563 | - Bearer: [] 564 | summary: 删除菜单 565 | tags: 566 | - 菜单模块 567 | get: 568 | consumes: 569 | - application/json 570 | description: 查询单个菜单信息 571 | parameters: 572 | - description: 菜单ID 573 | in: path 574 | name: id 575 | required: true 576 | type: integer 577 | produces: 578 | - application/json 579 | responses: 580 | "200": 581 | description: OK 582 | schema: 583 | $ref: '#/definitions/api.Response' 584 | security: 585 | - Bearer: [] 586 | summary: 查询菜单 587 | tags: 588 | - 菜单模块 589 | put: 590 | consumes: 591 | - application/json 592 | description: 修改单个菜单信息 593 | parameters: 594 | - description: 菜单ID 595 | in: path 596 | name: id 597 | required: true 598 | type: integer 599 | - description: params 600 | in: body 601 | name: request 602 | required: true 603 | schema: 604 | $ref: '#/definitions/menu.UpdateRequest' 605 | produces: 606 | - application/json 607 | responses: 608 | "200": 609 | description: OK 610 | schema: 611 | $ref: '#/definitions/api.Response' 612 | security: 613 | - Bearer: [] 614 | summary: 修改菜单 615 | tags: 616 | - 菜单模块 617 | /menu/option: 618 | get: 619 | consumes: 620 | - application/json 621 | description: 菜单下拉选项 622 | parameters: 623 | - description: 菜单类型 目录D 菜单M 按钮B 624 | in: query 625 | name: type 626 | required: true 627 | type: string 628 | produces: 629 | - application/json 630 | responses: 631 | "200": 632 | description: OK 633 | schema: 634 | items: 635 | $ref: '#/definitions/menu.Option' 636 | type: array 637 | security: 638 | - Bearer: [] 639 | summary: 菜单选项 640 | tags: 641 | - 菜单模块 642 | /record/login: 643 | get: 644 | consumes: 645 | - application/json 646 | description: 登录日志列表 647 | parameters: 648 | - in: query 649 | maxLength: 50 650 | name: email 651 | type: string 652 | - in: query 653 | maxLength: 60 654 | name: ip 655 | type: string 656 | - example: 1 657 | in: query 658 | minimum: 1 659 | name: page 660 | required: true 661 | type: integer 662 | - example: 10 663 | in: query 664 | maximum: 100 665 | minimum: 1 666 | name: size 667 | required: true 668 | type: integer 669 | produces: 670 | - application/json 671 | responses: 672 | "200": 673 | description: OK 674 | schema: 675 | $ref: '#/definitions/api.Response' 676 | security: 677 | - Bearer: [] 678 | summary: 登录日志 679 | tags: 680 | - 日志模块 681 | /role: 682 | get: 683 | consumes: 684 | - application/json 685 | description: 查询角色列表 686 | parameters: 687 | - description: 角色名称 688 | example: test 689 | in: query 690 | maxLength: 50 691 | name: name 692 | type: string 693 | produces: 694 | - application/json 695 | responses: 696 | "200": 697 | description: OK 698 | schema: 699 | $ref: '#/definitions/api.Response' 700 | security: 701 | - Bearer: [] 702 | summary: 角色列表 703 | tags: 704 | - 角色模块 705 | post: 706 | consumes: 707 | - application/json 708 | description: 创建角色 709 | parameters: 710 | - description: params 711 | in: body 712 | name: request 713 | required: true 714 | schema: 715 | $ref: '#/definitions/role.CreateRequest' 716 | produces: 717 | - application/json 718 | responses: 719 | "200": 720 | description: OK 721 | schema: 722 | $ref: '#/definitions/api.Response' 723 | security: 724 | - Bearer: [] 725 | summary: 创建角色 726 | tags: 727 | - 角色模块 728 | /role/{id}: 729 | delete: 730 | consumes: 731 | - application/json 732 | description: 删除单个角色 733 | parameters: 734 | - description: 角色ID 735 | in: path 736 | name: id 737 | required: true 738 | type: integer 739 | produces: 740 | - application/json 741 | responses: 742 | "200": 743 | description: OK 744 | schema: 745 | $ref: '#/definitions/api.Response' 746 | security: 747 | - Bearer: [] 748 | summary: 删除角色 749 | tags: 750 | - 角色模块 751 | get: 752 | consumes: 753 | - application/json 754 | description: 查询单个角色信息 755 | parameters: 756 | - description: 角色ID 757 | in: path 758 | name: id 759 | required: true 760 | type: integer 761 | produces: 762 | - application/json 763 | responses: 764 | "200": 765 | description: OK 766 | schema: 767 | $ref: '#/definitions/api.Response' 768 | security: 769 | - Bearer: [] 770 | summary: 查询角色 771 | tags: 772 | - 角色模块 773 | put: 774 | consumes: 775 | - application/json 776 | description: 修改单个角色信息 777 | parameters: 778 | - description: 角色ID 779 | in: path 780 | name: id 781 | required: true 782 | type: integer 783 | - description: params 784 | in: body 785 | name: request 786 | required: true 787 | schema: 788 | $ref: '#/definitions/role.UpdateRequest' 789 | produces: 790 | - application/json 791 | responses: 792 | "200": 793 | description: OK 794 | schema: 795 | $ref: '#/definitions/api.Response' 796 | security: 797 | - Bearer: [] 798 | summary: 修改角色 799 | tags: 800 | - 角色模块 801 | /role/{id}/menu: 802 | put: 803 | consumes: 804 | - application/json 805 | description: 修改单个角色权限 806 | parameters: 807 | - description: 角色ID 808 | in: path 809 | name: id 810 | required: true 811 | type: integer 812 | - description: params 813 | in: body 814 | name: request 815 | required: true 816 | schema: 817 | $ref: '#/definitions/role.MenuRequest' 818 | produces: 819 | - application/json 820 | responses: 821 | "200": 822 | description: OK 823 | schema: 824 | $ref: '#/definitions/api.Response' 825 | security: 826 | - Bearer: [] 827 | summary: 修改角色权限 828 | tags: 829 | - 角色模块 830 | /user: 831 | get: 832 | consumes: 833 | - application/json 834 | description: 搜索员工 835 | parameters: 836 | - description: 部门ID 837 | example: 1 838 | in: query 839 | name: deptId 840 | type: integer 841 | - description: 邮箱 842 | example: admin@helloadmin.com 843 | in: query 844 | name: email 845 | type: string 846 | - description: 员工昵称 847 | example: admin 848 | in: query 849 | name: nickname 850 | type: string 851 | - description: 页码 852 | example: 1 853 | in: query 854 | minimum: 1 855 | name: page 856 | required: true 857 | type: integer 858 | - description: 角色ID 859 | example: 1 860 | in: query 861 | name: roleId 862 | type: integer 863 | - description: 每页条数 864 | example: 10 865 | in: query 866 | minimum: 1 867 | name: size 868 | required: true 869 | type: integer 870 | produces: 871 | - application/json 872 | responses: 873 | "200": 874 | description: OK 875 | schema: 876 | $ref: '#/definitions/api.Response' 877 | security: 878 | - Bearer: [] 879 | summary: 搜索员工 880 | tags: 881 | - 员工模块 882 | post: 883 | consumes: 884 | - application/json 885 | description: 添加员工 886 | parameters: 887 | - description: params 888 | in: body 889 | name: request 890 | required: true 891 | schema: 892 | $ref: '#/definitions/user.RegisterRequest' 893 | produces: 894 | - application/json 895 | responses: 896 | "200": 897 | description: OK 898 | schema: 899 | $ref: '#/definitions/api.Response' 900 | security: 901 | - Bearer: [] 902 | summary: 添加员工 903 | tags: 904 | - 员工模块 905 | /user/{id}: 906 | delete: 907 | consumes: 908 | - application/json 909 | produces: 910 | - application/json 911 | responses: 912 | "200": 913 | description: OK 914 | schema: 915 | $ref: '#/definitions/api.Response' 916 | security: 917 | - Bearer: [] 918 | summary: 删除员工信息 919 | tags: 920 | - 员工模块 921 | get: 922 | consumes: 923 | - application/json 924 | produces: 925 | - application/json 926 | responses: 927 | "200": 928 | description: OK 929 | schema: 930 | $ref: '#/definitions/user.ProfileData' 931 | security: 932 | - Bearer: [] 933 | summary: 获取员工信息 934 | tags: 935 | - 员工模块 936 | put: 937 | consumes: 938 | - application/json 939 | parameters: 940 | - description: params 941 | in: body 942 | name: request 943 | required: true 944 | schema: 945 | $ref: '#/definitions/user.UpdateRequest' 946 | produces: 947 | - application/json 948 | responses: 949 | "200": 950 | description: OK 951 | schema: 952 | $ref: '#/definitions/api.Response' 953 | security: 954 | - Bearer: [] 955 | summary: 修改员工信息 956 | tags: 957 | - 员工模块 958 | /user/profile: 959 | get: 960 | consumes: 961 | - application/json 962 | produces: 963 | - application/json 964 | responses: 965 | "200": 966 | description: OK 967 | schema: 968 | $ref: '#/definitions/user.ProfileData' 969 | security: 970 | - Bearer: [] 971 | summary: 登录账号信息 972 | tags: 973 | - 员工模块 974 | securityDefinitions: 975 | Bearer: 976 | in: header 977 | name: Authorization 978 | type: apiKey 979 | swagger: "2.0" 980 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module helloadmin 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.0 7 | github.com/gin-gonic/gin v1.9.1 8 | github.com/go-co-op/gocron v1.28.2 9 | github.com/go-redis/redismock/v9 v9.0.3 10 | github.com/golang-jwt/jwt/v5 v5.0.0 11 | github.com/golang/mock v1.6.0 12 | github.com/google/uuid v1.3.0 13 | github.com/google/wire v0.5.0 14 | github.com/mssola/user_agent v0.6.0 15 | github.com/redis/go-redis/v9 v9.0.5 16 | github.com/sony/sonyflake v1.1.0 17 | github.com/spf13/viper v1.16.0 18 | github.com/stretchr/testify v1.8.4 19 | github.com/swaggo/files v1.0.1 20 | github.com/swaggo/gin-swagger v1.6.0 21 | github.com/swaggo/swag v1.16.2 22 | go.uber.org/zap v1.26.0 23 | golang.org/x/crypto v0.18.0 24 | google.golang.org/grpc v1.55.0 25 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 26 | gorm.io/driver/mysql v1.5.1 27 | gorm.io/gorm v1.25.5 28 | moul.io/zapgorm2 v1.3.0 29 | ) 30 | 31 | require ( 32 | github.com/KyleBanks/depth v1.2.1 // indirect 33 | github.com/bytedance/sonic v1.9.1 // indirect 34 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 35 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 36 | github.com/davecgh/go-spew v1.1.1 // indirect 37 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 38 | github.com/fsnotify/fsnotify v1.6.0 // indirect 39 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 40 | github.com/gin-contrib/sse v0.1.0 // indirect 41 | github.com/go-openapi/jsonpointer v0.20.0 // indirect 42 | github.com/go-openapi/jsonreference v0.20.2 // indirect 43 | github.com/go-openapi/spec v0.20.9 // indirect 44 | github.com/go-openapi/swag v0.22.4 // indirect 45 | github.com/go-playground/locales v0.14.1 // indirect 46 | github.com/go-playground/universal-translator v0.18.1 // indirect 47 | github.com/go-playground/validator/v10 v10.14.0 // indirect 48 | github.com/go-sql-driver/mysql v1.7.0 // indirect 49 | github.com/goccy/go-json v0.10.2 // indirect 50 | github.com/golang/protobuf v1.5.3 // indirect 51 | github.com/hashicorp/hcl v1.0.0 // indirect 52 | github.com/jinzhu/inflection v1.0.0 // indirect 53 | github.com/jinzhu/now v1.1.5 // indirect 54 | github.com/josharian/intern v1.0.0 // indirect 55 | github.com/json-iterator/go v1.1.12 // indirect 56 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 57 | github.com/leodido/go-urn v1.2.4 // indirect 58 | github.com/magiconair/properties v1.8.7 // indirect 59 | github.com/mailru/easyjson v0.7.7 // indirect 60 | github.com/mattn/go-isatty v0.0.19 // indirect 61 | github.com/mitchellh/mapstructure v1.5.0 // indirect 62 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 63 | github.com/modern-go/reflect2 v1.0.2 // indirect 64 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 65 | github.com/pmezard/go-difflib v1.0.0 // indirect 66 | github.com/robfig/cron/v3 v3.0.1 // indirect 67 | github.com/spf13/afero v1.9.5 // indirect 68 | github.com/spf13/cast v1.5.1 // indirect 69 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 70 | github.com/spf13/pflag v1.0.5 // indirect 71 | github.com/subosito/gotenv v1.4.2 // indirect 72 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 73 | github.com/ugorji/go/codec v1.2.11 // indirect 74 | go.uber.org/atomic v1.11.0 // indirect 75 | go.uber.org/multierr v1.11.0 // indirect 76 | golang.org/x/arch v0.3.0 // indirect 77 | golang.org/x/net v0.20.0 // indirect 78 | golang.org/x/sys v0.16.0 // indirect 79 | golang.org/x/text v0.14.0 // indirect 80 | golang.org/x/tools v0.17.0 // indirect 81 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 82 | google.golang.org/protobuf v1.30.0 // indirect 83 | gopkg.in/ini.v1 v1.67.0 // indirect 84 | gopkg.in/yaml.v3 v3.0.1 // indirect 85 | ) 86 | -------------------------------------------------------------------------------- /images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heliosker/helloadmin/e37641503ebdb7de3584fcbeea89914c9adb8e05/images/dashboard.png -------------------------------------------------------------------------------- /images/dept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heliosker/helloadmin/e37641503ebdb7de3584fcbeea89914c9adb8e05/images/dept.png -------------------------------------------------------------------------------- /images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heliosker/helloadmin/e37641503ebdb7de3584fcbeea89914c9adb8e05/images/login.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heliosker/helloadmin/e37641503ebdb7de3584fcbeea89914c9adb8e05/images/logo.png -------------------------------------------------------------------------------- /images/role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heliosker/helloadmin/e37641503ebdb7de3584fcbeea89914c9adb8e05/images/role.png -------------------------------------------------------------------------------- /internal/api/response.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "helloadmin/internal/ecode" 9 | ) 10 | 11 | type Response struct { 12 | Code int `json:"code"` 13 | Message string `json:"message"` 14 | Data interface{} `json:"data"` 15 | } 16 | 17 | type Pagination struct { 18 | Page int `json:"page" binding:"required,min=1" example:"1"` 19 | Size int `json:"size" binding:"required,min=1,max=100" example:"10"` 20 | Count int `json:"count"` 21 | } 22 | 23 | func Success(ctx *gin.Context, data interface{}) { 24 | if data == nil { 25 | data = map[string]interface{}{} 26 | } 27 | e := ecode.ErrSuccess 28 | resp := Response{Code: e.Int(), Message: e.Error(), Data: data} 29 | ctx.JSON(http.StatusOK, resp) 30 | } 31 | 32 | func Error(ctx *gin.Context, httpCode int, err error) { 33 | var c ecode.ErrCode 34 | var m string 35 | switch { 36 | case errors.As(err, &c): 37 | c = c.Code() 38 | m = c.String() 39 | default: 40 | c = ecode.ErrInternalServer 41 | m = err.Error() 42 | } 43 | resp := Response{Code: c.Int(), Message: m} 44 | ctx.JSON(httpCode, resp) 45 | } 46 | -------------------------------------------------------------------------------- /internal/department/api_types.go: -------------------------------------------------------------------------------- 1 | package department 2 | 3 | type ResponseItem struct { 4 | ID uint `json:"id"` 5 | Name string `json:"name"` 6 | Sort int `json:"sort"` 7 | ParentId uint `json:"parentId"` 8 | Leader string `json:"leader"` 9 | UpdateAt string `json:"updateAt"` 10 | CreatedAt string `json:"createdAt"` 11 | Children []ResponseItem `json:"children"` 12 | } 13 | 14 | type FindRequest struct { 15 | Name string `form:"name" binding:"max=50"` // 部门名称 16 | } 17 | 18 | type CreateRequest struct { 19 | Name string `json:"name" binding:"required"` // 部门名称 20 | Sort int `json:"sort" binding:"required"` // 排序值,值越大,显示顺序越靠前 21 | Leader string `json:"leader"` // 部门负责人 22 | ParentId uint `json:"parentId"` // 上级部门 23 | } 24 | 25 | type UpdateRequest struct { 26 | Name string `json:"name"` 27 | Sort int `json:"sort"` 28 | ParentId uint `json:"parentId"` 29 | Leader string `json:"leader"` 30 | } 31 | 32 | type Response struct { 33 | Items []ResponseItem `json:"items"` 34 | } 35 | -------------------------------------------------------------------------------- /internal/department/handler.go: -------------------------------------------------------------------------------- 1 | package department 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "go.uber.org/zap" 9 | "helloadmin/internal/api" 10 | "helloadmin/pkg/log" 11 | ) 12 | 13 | type Handler struct { 14 | log *log.Logger 15 | svc Service 16 | } 17 | 18 | func NewHandler(log *log.Logger, svc Service) *Handler { 19 | return &Handler{ 20 | log: log, 21 | svc: svc, 22 | } 23 | } 24 | 25 | // StoreDepartment godoc 26 | // @Summary 创建部门 27 | // @Schemes 28 | // @Description 创建部门 29 | // @Tags 部门模块 30 | // @Accept json 31 | // @Produce json 32 | // @Security Bearer 33 | // @Param request body CreateRequest true "params" 34 | // @Success 200 {object} Response 35 | // @Router /department [post] 36 | func (d *Handler) StoreDepartment(ctx *gin.Context) { 37 | req := new(CreateRequest) 38 | if err := ctx.ShouldBindJSON(req); err != nil { 39 | api.Error(ctx, http.StatusBadRequest, err) 40 | return 41 | } 42 | if err := d.svc.CreateDepartment(ctx, req); err != nil { 43 | d.log.WithContext(ctx).Error("svc.CreateDepartment error", zap.Error(err)) 44 | api.Error(ctx, http.StatusInternalServerError, err) 45 | return 46 | } 47 | api.Success(ctx, nil) 48 | } 49 | 50 | // GetDepartment godoc 51 | // @Summary 部门列表 52 | // @Schemes 53 | // @Description 查询部门列表 54 | // @Tags 部门模块 55 | // @Accept json 56 | // @Produce json 57 | // @Security Bearer 58 | // @Param request query FindRequest true "params" 59 | // @Success 200 {object} Response 60 | // @Router /department [get] 61 | func (d *Handler) GetDepartment(ctx *gin.Context) { 62 | req := new(FindRequest) 63 | if err := ctx.ShouldBindQuery(req); err != nil { 64 | d.log.WithContext(ctx).Error("DepartmentHandler.GetDepartment error", zap.Error(err)) 65 | api.Error(ctx, http.StatusBadRequest, err) 66 | return 67 | } 68 | departments, err := d.svc.SearchDepartment(ctx, req) 69 | if err != nil { 70 | d.log.WithContext(ctx).Error("svc.SearchDepartment error", zap.Error(err)) 71 | api.Error(ctx, http.StatusInternalServerError, err) 72 | return 73 | } 74 | api.Success(ctx, departments) 75 | } 76 | 77 | // ShowDepartment godoc 78 | // @Summary 查询部门 79 | // @Schemes 80 | // @Description 查询单个部门信息 81 | // @Tags 部门模块 82 | // @Accept json 83 | // @Produce json 84 | // @Security Bearer 85 | // @Param id path int true "部门ID" 86 | // @Success 200 {object} Response 87 | // @Router /department/{id} [get] 88 | func (d *Handler) ShowDepartment(ctx *gin.Context) { 89 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 90 | if department, err := d.svc.GetDepartmentById(ctx, id); err != nil { 91 | d.log.WithContext(ctx).Error("svc.GetDepartmentById error", zap.Error(err)) 92 | api.Error(ctx, http.StatusInternalServerError, err) 93 | return 94 | } else { 95 | api.Success(ctx, department) 96 | } 97 | } 98 | 99 | // UpdateDepartment godoc 100 | // @Summary 修改部门 101 | // @Schemes 102 | // @Description 修改单个部门信息 103 | // @Tags 部门模块 104 | // @Accept json 105 | // @Produce json 106 | // @Security Bearer 107 | // @Param id path int true "部门ID" 108 | // @Param request body UpdateRequest true "params" 109 | // @Success 200 {object} Response 110 | // @Router /department/{id} [put] 111 | func (d *Handler) UpdateDepartment(ctx *gin.Context) { 112 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 113 | req := new(UpdateRequest) 114 | if err := ctx.ShouldBindJSON(req); err != nil { 115 | d.log.WithContext(ctx).Error("DepartmentHandler.UpdateDepartment error", zap.Error(err)) 116 | api.Error(ctx, http.StatusBadRequest, err) 117 | return 118 | } 119 | if err := d.svc.UpdateDepartment(ctx, id, req); err != nil { 120 | d.log.WithContext(ctx).Error("svc.UpdateDepartment error", zap.Error(err)) 121 | api.Error(ctx, http.StatusInternalServerError, err) 122 | return 123 | } 124 | api.Success(ctx, nil) 125 | } 126 | 127 | // DeleteDepartment godoc 128 | // @Summary 删除部门 129 | // @Schemes 130 | // @Description 删除单个部门 131 | // @Tags 部门模块 132 | // @Accept json 133 | // @Produce json 134 | // @Security Bearer 135 | // @Param id path int true "部门ID" 136 | // @Success 200 {object} Response 137 | // @Router /department/{id} [delete] 138 | func (d *Handler) DeleteDepartment(ctx *gin.Context) { 139 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 140 | if err := d.svc.DeleteDepartment(ctx, id); err != nil { 141 | d.log.WithContext(ctx).Error("svc.DeleteDepartment error", zap.Error(err)) 142 | api.Error(ctx, http.StatusInternalServerError, err) 143 | return 144 | } 145 | api.Success(ctx, nil) 146 | } 147 | -------------------------------------------------------------------------------- /internal/department/model.go: -------------------------------------------------------------------------------- 1 | package department 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Model struct { 8 | ID uint `gorm:"primaryKey" json:"id"` 9 | Name string `json:"name" gorm:"type:varchar(30);not null;default:'';comment:部门名称"` 10 | ParentId uint `json:"parent_id" gorm:"type:int;default:0;comment:上级部门ID"` 11 | Sort int `json:"sort" gorm:"type:int; default:0;comment:排序值,值越大越靠前"` 12 | Leader string `json:"leader" gorm:"type:varchar(60);not null;default:'';comment:部门负责人"` 13 | CreatedAt time.Time `json:"created_at,omitempty" gorm:"default:null;comment:创建于"` 14 | UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"default:null;comment:更新于"` 15 | } 16 | 17 | func (u *Model) TableName() string { 18 | return "department" 19 | } 20 | -------------------------------------------------------------------------------- /internal/department/repository.go: -------------------------------------------------------------------------------- 1 | package department 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "gorm.io/gorm" 7 | "helloadmin/internal/ecode" 8 | "helloadmin/internal/repository" 9 | ) 10 | 11 | type Repository interface { 12 | Find(ctx context.Context, request *FindRequest) (int64, *[]Model, error) 13 | GetById(ctx context.Context, id int64) (*Model, error) 14 | GetByParentId(ctx context.Context, id int64) (*[]Model, error) 15 | Create(ctx context.Context, department *Model) error 16 | Update(ctx context.Context, id int64, department *Model) error 17 | Delete(ctx context.Context, id int64) error 18 | } 19 | 20 | func NewRepository(r *repository.Repository) Repository { 21 | return &departmentRepository{ 22 | Repository: r, 23 | } 24 | } 25 | 26 | type departmentRepository struct { 27 | *repository.Repository 28 | } 29 | 30 | func (r *departmentRepository) Find(ctx context.Context, req *FindRequest) (int64, *[]Model, error) { 31 | var count int64 32 | var departments []Model 33 | query := r.DB(ctx) 34 | if req.Name != "" { 35 | query = query.Where("name = ?", req.Name) 36 | } 37 | query.Model(Model{}).Count(&count) 38 | if err := query.Order("sort DESC").Find(&departments).Error; err != nil { 39 | return count, nil, err 40 | } 41 | return count, &departments, nil 42 | } 43 | 44 | func (r *departmentRepository) GetById(ctx context.Context, id int64) (*Model, error) { 45 | var department Model 46 | if err := r.DB(ctx).Where("id = ?", id).First(&department).Error; err != nil { 47 | if errors.Is(err, gorm.ErrRecordNotFound) { 48 | return nil, ecode.ErrDeptNotFound 49 | } 50 | return nil, err 51 | } 52 | return &department, nil 53 | } 54 | 55 | func (r *departmentRepository) GetByParentId(ctx context.Context, id int64) (*[]Model, error) { 56 | var departments []Model 57 | if err := r.DB(ctx).Where("parent_id = ?", id).Find(&departments).Error; err != nil { 58 | if errors.Is(err, gorm.ErrRecordNotFound) { 59 | return nil, nil 60 | } 61 | return nil, err 62 | } 63 | return &departments, nil 64 | } 65 | 66 | func (r *departmentRepository) Create(ctx context.Context, department *Model) error { 67 | if err := r.DB(ctx).Create(department).Error; err != nil { 68 | return err 69 | } 70 | return nil 71 | } 72 | 73 | func (r *departmentRepository) Update(ctx context.Context, id int64, department *Model) error { 74 | if err := r.DB(ctx).Model(department).Select("name", "parent_id", "sort", "leader").Where("id = ?", id).Updates(department).Error; err != nil { 75 | return err 76 | } 77 | return nil 78 | } 79 | 80 | func (r *departmentRepository) Delete(ctx context.Context, id int64) error { 81 | if err := r.DB(ctx).Delete(&Model{}, id).Error; err != nil { 82 | return err 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /internal/department/service.go: -------------------------------------------------------------------------------- 1 | package department 2 | 3 | import ( 4 | "context" 5 | //"helloadmin/internal/user" 6 | "time" 7 | 8 | "helloadmin/internal/ecode" 9 | ) 10 | 11 | type Service interface { 12 | GetDepartmentById(ctx context.Context, id int64) (*ResponseItem, error) 13 | SearchDepartment(ctx context.Context, request *FindRequest) (*Response, error) 14 | CreateDepartment(ctx context.Context, request *CreateRequest) error 15 | UpdateDepartment(ctx context.Context, id int64, request *UpdateRequest) error 16 | DeleteDepartment(ctx context.Context, id int64) error 17 | } 18 | 19 | func NewService(repo Repository) Service { 20 | return &service{ 21 | repo: repo, 22 | } 23 | } 24 | 25 | type service struct { 26 | repo Repository 27 | } 28 | 29 | func (s *service) GetDepartmentById(ctx context.Context, id int64) (*ResponseItem, error) { 30 | department, err := s.repo.GetById(ctx, id) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return &ResponseItem{ 35 | ID: department.ID, 36 | Name: department.Name, 37 | ParentId: department.ParentId, 38 | Sort: department.Sort, 39 | Leader: department.Leader, 40 | CreatedAt: department.CreatedAt.Format(time.DateTime), 41 | UpdateAt: department.UpdatedAt.Format(time.DateTime), 42 | }, nil 43 | } 44 | 45 | func (s *service) SearchDepartment(ctx context.Context, req *FindRequest) (*Response, error) { 46 | var result Response 47 | _, departs, err := s.repo.Find(ctx, req) 48 | if err != nil { 49 | return nil, err 50 | } 51 | result.Items = buildTree(departs, 0) 52 | return &result, nil 53 | } 54 | 55 | func buildTree(deptList *[]Model, parentId uint) []ResponseItem { 56 | result := make([]ResponseItem, 0) 57 | if len(*deptList) > 0 { 58 | for _, depart := range *deptList { 59 | if depart.ParentId == parentId { 60 | child := ResponseItem{ 61 | ID: depart.ID, 62 | Name: depart.Name, 63 | ParentId: depart.ParentId, 64 | Sort: depart.Sort, 65 | Leader: depart.Leader, 66 | CreatedAt: depart.CreatedAt.Format(time.DateTime), 67 | UpdateAt: depart.UpdatedAt.Format(time.DateTime), 68 | } 69 | child.Children = buildTree(deptList, depart.ID) 70 | result = append(result, child) 71 | } 72 | } 73 | } 74 | return result 75 | } 76 | 77 | func (s *service) CreateDepartment(ctx context.Context, req *CreateRequest) error { 78 | if req.ParentId > 0 { 79 | if dept, _ := s.repo.GetById(ctx, int64(req.ParentId)); dept == nil { 80 | return ecode.ErrDeptParentNotFound 81 | } 82 | } 83 | department := Model{ 84 | Name: req.Name, 85 | ParentId: req.ParentId, 86 | Leader: req.Leader, 87 | Sort: req.Sort, 88 | UpdatedAt: time.Now(), 89 | CreatedAt: time.Now(), 90 | } 91 | return s.repo.Create(ctx, &department) 92 | } 93 | 94 | func (s *service) UpdateDepartment(ctx context.Context, id int64, req *UpdateRequest) error { 95 | if req.ParentId > 0 { 96 | if dept, _ := s.repo.GetById(ctx, int64(req.ParentId)); dept == nil { 97 | return ecode.ErrDeptParentNotFound 98 | } 99 | } 100 | department := Model{ 101 | Name: req.Name, 102 | ParentId: req.ParentId, 103 | Leader: req.Leader, 104 | Sort: req.Sort, 105 | UpdatedAt: time.Now(), 106 | } 107 | return s.repo.Update(ctx, id, &department) 108 | } 109 | 110 | func (s *service) DeleteDepartment(ctx context.Context, id int64) error { 111 | if departments, _ := s.repo.GetByParentId(ctx, id); len(*departments) > 0 { 112 | return ecode.ErrDeptHasChild 113 | } 114 | return s.repo.Delete(ctx, id) 115 | } 116 | -------------------------------------------------------------------------------- /internal/ecode/errcode_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ErrCode -linecomment"; DO NOT EDIT. 2 | 3 | package ecode 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ErrSuccess-0] 12 | _ = x[ErrBadRequest-400] 13 | _ = x[ErrUnauthorized-401] 14 | _ = x[ErrNotFound-404] 15 | _ = x[ErrInternalServer-500] 16 | _ = x[ErrEmailAlreadyUse-10006] 17 | _ = x[ErrPasswordIncorrect-10007] 18 | _ = x[ErrUserNotFound-10008] 19 | _ = x[ErrAdminUserCanNotModify-10009] 20 | _ = x[ErrRoleHasUser-10010] 21 | _ = x[ErrRoleNotFound-10011] 22 | _ = x[ErrMenuHasChild-10012] 23 | _ = x[ErrMenuParentedNotFound-10013] 24 | _ = x[ErrDeptNotFound-10014] 25 | _ = x[ErrDeptHasChild-10015] 26 | _ = x[ErrDeptHasUser-10016] 27 | _ = x[ErrDeptParentNotFound-10017] 28 | } 29 | 30 | const ( 31 | _ErrCode_name_0 = "Success" 32 | _ErrCode_name_1 = "Bad RequestUnauthorized" 33 | _ErrCode_name_2 = "Not Found" 34 | _ErrCode_name_3 = "Internal Server Error" 35 | _ErrCode_name_4 = "The email is already in useThe password is incorrectThe user does not existThe super administrator role cannot be modifiedThe role has users and cannot be deletedThe role not foundThe menu has children and cannot be deletedThe parent menu not foundThe department not foundThe department has children and cannot be deletedThe department has user and cannot be deletedThe parent department not found" 36 | ) 37 | 38 | var ( 39 | _ErrCode_index_1 = [...]uint8{0, 11, 23} 40 | _ErrCode_index_4 = [...]uint16{0, 27, 52, 75, 122, 162, 180, 223, 248, 272, 321, 366, 397} 41 | ) 42 | 43 | func (i ErrCode) String() string { 44 | switch { 45 | case i == 0: 46 | return _ErrCode_name_0 47 | case 400 <= i && i <= 401: 48 | i -= 400 49 | return _ErrCode_name_1[_ErrCode_index_1[i]:_ErrCode_index_1[i+1]] 50 | case i == 404: 51 | return _ErrCode_name_2 52 | case i == 500: 53 | return _ErrCode_name_3 54 | case 10006 <= i && i <= 10017: 55 | i -= 10006 56 | return _ErrCode_name_4[_ErrCode_index_4[i]:_ErrCode_index_4[i+1]] 57 | default: 58 | return "ErrCode(" + strconv.FormatInt(int64(i), 10) + ")" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/ecode/error.go: -------------------------------------------------------------------------------- 1 | package ecode 2 | 3 | //go:generate stringer -type ErrCode -linecomment 4 | 5 | type ErrCode int 6 | 7 | func (e ErrCode) Int() int { 8 | return int(e) 9 | } 10 | 11 | func (e ErrCode) Code() ErrCode { 12 | return e 13 | } 14 | 15 | func (e ErrCode) Error() string { 16 | return e.String() 17 | } 18 | 19 | const ( 20 | ErrSuccess ErrCode = 0 // Success 21 | ErrBadRequest ErrCode = 400 // Bad Request 22 | ErrUnauthorized ErrCode = 401 // Unauthorized 23 | ErrNotFound ErrCode = 404 // Not Found 24 | ErrInternalServer ErrCode = 500 // Internal Server Error 25 | 26 | ErrEmailAlreadyUse ErrCode = iota + 10001 // The email is already in use 27 | ErrPasswordIncorrect // The password is incorrect 28 | ErrUserNotFound // The user does not exist 29 | ErrAdminUserCanNotModify // The super administrator role cannot be modified 30 | ErrRoleHasUser // The role has users and cannot be deleted 31 | ErrRoleNotFound // The role not found 32 | ErrMenuHasChild // The menu has children and cannot be deleted 33 | ErrMenuParentedNotFound // The parent menu not found 34 | ErrDeptNotFound // The department not found 35 | ErrDeptHasChild // The department has children and cannot be deleted 36 | ErrDeptHasUser // The department has user and cannot be deleted 37 | ErrDeptParentNotFound // The parent department not found 38 | ) 39 | -------------------------------------------------------------------------------- /internal/login_record/api_types.go: -------------------------------------------------------------------------------- 1 | package login_record 2 | 3 | import ( 4 | "helloadmin/internal/api" 5 | ) 6 | 7 | type CreateRequest struct { 8 | Ip string `json:"ip" binding:"max=60"` 9 | Os string `json:"os"` 10 | Email string `json:"email"` 11 | Browser string `json:"browser"` 12 | Platform string `json:"platform"` 13 | UserName string `json:"userName"` 14 | ErrorMessage string `json:"ErrorMessage"` 15 | } 16 | 17 | type FindRequest struct { 18 | Ip string `form:"ip" binding:"max=60"` 19 | Email string `form:"email" binding:"max=50"` 20 | Page int `form:"page" binding:"required,min=1" example:"1"` 21 | Size int `form:"size" binding:"required,min=1,max=100" example:"10"` 22 | } 23 | 24 | type Item struct { 25 | Ip string `json:"ip"` 26 | Os string `json:"os"` 27 | Email string `json:"email"` 28 | UserName string `json:"userName"` 29 | Browser string `json:"browser"` 30 | Platform string `json:"platform"` 31 | ErrorMessage string `json:"ErrorMessage"` 32 | UpdatedAt string `json:"updatedAt"` 33 | CreatedAt string `json:"createdAt"` 34 | } 35 | 36 | type Response struct { 37 | Items []Item `json:"items"` 38 | api.Pagination `json:"pagination"` 39 | } 40 | -------------------------------------------------------------------------------- /internal/login_record/handler.go: -------------------------------------------------------------------------------- 1 | package login_record 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "go.uber.org/zap" 8 | "helloadmin/internal/api" 9 | "helloadmin/pkg/log" 10 | ) 11 | 12 | type Handler struct { 13 | log *log.Logger 14 | svc Service 15 | } 16 | 17 | func NewHandler(log *log.Logger, svc Service) *Handler { 18 | return &Handler{ 19 | log: log, 20 | svc: svc, 21 | } 22 | } 23 | 24 | // SearchLoginRecord godoc 25 | // @Summary 登录日志 26 | // @Schemes 27 | // @Description 登录日志列表 28 | // @Tags 日志模块 29 | // @Accept json 30 | // @Produce json 31 | // @Security Bearer 32 | // @Param request query FindRequest true "params" 33 | // @Success 200 {object} api.Response 34 | // @Router /record/login [get] 35 | func (lrh *Handler) SearchLoginRecord(ctx *gin.Context) { 36 | req := new(FindRequest) 37 | if err := ctx.ShouldBind(req); err != nil { 38 | lrh.log.WithContext(ctx).Error("SearchLoginRecord ShouldBind error", zap.Error(err)) 39 | api.Error(ctx, http.StatusBadRequest, err) 40 | return 41 | } 42 | if res, err := lrh.svc.Search(ctx, req); err != nil { 43 | api.Error(ctx, http.StatusInternalServerError, err) 44 | } else { 45 | api.Success(ctx, res) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/login_record/model.go: -------------------------------------------------------------------------------- 1 | package login_record 2 | 3 | import "time" 4 | 5 | type Model struct { 6 | ID uint `json:"id" gorm:"primaryKey"` 7 | Ip string `json:"ip" gorm:"type:varchar(60);not null;default:'';comment:客户端IP"` 8 | Os string `json:"os" gorm:"type:varchar(60);comment:操作系统"` 9 | Email string `json:"email" gorm:"type:varchar(60);not null;default:'';comment:登录邮箱"` 10 | Browser string `json:"browser" gorm:"type:varchar(60);not null;default:'';comment:浏览器"` 11 | Platform string `json:"platform" gorm:"type:varchar(60);comment:平台"` 12 | ErrorMessage string `json:"errorMessage" gorm:"type:varchar(255);comment:错误信息"` 13 | CreatedAt time.Time `json:"created_at,omitempty" gorm:"default:null;comment:创建于"` 14 | UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"default:null;comment:更新于"` 15 | } 16 | 17 | func (m *Model) TableName() string { 18 | return "login_record" 19 | } 20 | -------------------------------------------------------------------------------- /internal/login_record/respository.go: -------------------------------------------------------------------------------- 1 | package login_record 2 | 3 | import ( 4 | "context" 5 | 6 | "helloadmin/internal/repository" 7 | ) 8 | 9 | type Repository interface { 10 | Create(ctx context.Context, record *Model) error 11 | Search(ctx context.Context, request *FindRequest) (int64, *[]Model, error) 12 | } 13 | 14 | func NewRepository(r *repository.Repository) Repository { 15 | return &loginRecordRepository{r} 16 | } 17 | 18 | type loginRecordRepository struct { 19 | *repository.Repository 20 | } 21 | 22 | func (lr *loginRecordRepository) Create(ctx context.Context, record *Model) error { 23 | if err := lr.DB(ctx).Create(record).Error; err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | 29 | func (lr *loginRecordRepository) Search(ctx context.Context, req *FindRequest) (int64, *[]Model, error) { 30 | var count int64 31 | var record []Model 32 | query := lr.DB(ctx) 33 | if req.Email != "" { 34 | query = query.Where("email = ?", req.Email) 35 | } 36 | if req.Ip != "" { 37 | query = query.Where("ip = ?", req.Ip) 38 | } 39 | if req.Page > 0 { 40 | query = query.Offset((req.Page - 1) * req.Size).Limit(req.Size) 41 | } 42 | query.Model(Model{}).Count(&count) 43 | if result := query.Order("id desc").Find(&record); result.Error != nil { 44 | return count, nil, result.Error 45 | } 46 | return count, &record, nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/login_record/service.go: -------------------------------------------------------------------------------- 1 | package login_record 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "helloadmin/internal/api" 8 | ) 9 | 10 | type Service interface { 11 | Create(ctx context.Context, record *CreateRequest) error 12 | Search(ctx context.Context, request *FindRequest) (*Response, error) 13 | } 14 | 15 | func NewService(repo Repository) Service { 16 | return &loginRecordService{ 17 | loginRecordRepository: repo, 18 | } 19 | } 20 | 21 | type loginRecordService struct { 22 | loginRecordRepository Repository 23 | } 24 | 25 | func (lrs *loginRecordService) Create(ctx context.Context, req *CreateRequest) error { 26 | model := Model{ 27 | Ip: req.Ip, 28 | Os: req.Os, 29 | Email: req.Email, 30 | Browser: req.Browser, 31 | Platform: req.Platform, 32 | ErrorMessage: req.ErrorMessage, 33 | UpdatedAt: time.Now(), 34 | CreatedAt: time.Now(), 35 | } 36 | if err := lrs.loginRecordRepository.Create(ctx, &model); err != nil { 37 | return err 38 | } 39 | return nil 40 | } 41 | 42 | func (lrs *loginRecordService) Search(ctx context.Context, request *FindRequest) (*Response, error) { 43 | var result Response 44 | count, records, err := lrs.loginRecordRepository.Search(ctx, request) 45 | if err != nil { 46 | return nil, err 47 | } 48 | result.Items = make([]Item, 0) 49 | if count > 0 { 50 | for _, record := range *records { 51 | result.Items = append(result.Items, Item{ 52 | Ip: record.Ip, 53 | Os: record.Os, 54 | Email: record.Email, 55 | Browser: record.Browser, 56 | Platform: record.Platform, 57 | ErrorMessage: record.ErrorMessage, 58 | UpdatedAt: record.UpdatedAt.Format(time.DateTime), 59 | CreatedAt: record.CreatedAt.Format(time.DateTime), 60 | }) 61 | } 62 | } 63 | result.Pagination = api.Pagination{ 64 | Page: request.Page, 65 | Size: request.Size, 66 | Count: int(count), 67 | } 68 | return &result, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/menu/api_types.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | type CreateRequest struct { 4 | Name string `json:"name" binding:"max=128"` // 菜单名称 5 | Title string `json:"title" binding:"required,max=128"` // 菜单标题 6 | Icon string `json:"icon" binding:"max=128"` // 菜单图标 7 | Path string `json:"path" binding:"max=255"` // 菜单路径 8 | Type string `json:"type" binding:"required,eq=D|eq=M|eq=B"` // 菜单类型 目录D 菜单M 按钮B 9 | ParentId uint `json:"parentId" binding:"min=0"` // 上级菜单ID 10 | Component string `json:"component"` // 组件路径 11 | Sort int `json:"sort"` // 排序值,值越大越靠前 12 | Visible string `json:"visible" binding:"required,eq=Y|eq=N"` // 是否可见,Y可见 N不可见 13 | } 14 | 15 | type FindRequest struct { 16 | Name string `form:"name"` // 菜单名称 17 | Visible string `form:"visible"` // 是否可见,Y可见 N不可见 18 | } 19 | 20 | type ResponseItem struct { 21 | ID uint `json:"id"` 22 | Name string `json:"name"` 23 | Title string `json:"title"` 24 | Icon string `json:"icon"` 25 | Path string `json:"path"` 26 | Type string `json:"type"` 27 | ParentId uint `json:"parentId"` 28 | Component string `json:"component"` 29 | Sort int `json:"sort"` 30 | Visible string `json:"visible"` 31 | CreatedAt string `json:"createdAt"` 32 | UpdatedAt string `json:"updatedAt"` 33 | Children []ResponseItem `json:"children,omitempty"` 34 | } 35 | 36 | type OptionRequest struct { 37 | Type string `form:"type" binding:"required,eq=D|eq=M|eq=B"` // 菜单类型 目录D 菜单M 按钮B 38 | } 39 | 40 | type Option struct { 41 | Label string `json:"label"` 42 | Value uint `json:"value"` 43 | } 44 | 45 | type Response struct { 46 | Items []ResponseItem `json:"items"` 47 | } 48 | 49 | type UpdateRequest struct { 50 | Name string `json:"name"` // 菜单名称 51 | Title string `json:"title"` // 菜单标题 52 | Icon string `json:"icon"` // 菜单图标 53 | Path string `json:"path"` // 菜单路径 54 | Type string `json:"type"` // 菜单类型 目录D 菜单M 按钮B 55 | ParentId uint `json:"parentId"` // 上级菜单ID 56 | Component string `json:"component"` // 组件路径 57 | Sort int `json:"sort"` // 排序值,值越大越靠前 58 | Visible string `json:"visible"` // 是否可见,Y可见 N不可见 59 | } 60 | -------------------------------------------------------------------------------- /internal/menu/const.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | const ( 4 | TypeDirectory = "D" 5 | TypeMenu = "M" 6 | TypeButton = "B" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/menu/handler.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "go.uber.org/zap" 9 | "helloadmin/internal/api" 10 | "helloadmin/pkg/log" 11 | ) 12 | 13 | type Handler struct { 14 | log *log.Logger 15 | svc Service 16 | } 17 | 18 | func NewHandler(log *log.Logger, svc Service) *Handler { 19 | return &Handler{ 20 | log: log, 21 | svc: svc, 22 | } 23 | } 24 | 25 | // StoreMenu godoc 26 | // @Summary 创建菜单 27 | // @Schemes 28 | // @Description 创建菜单 29 | // @Tags 菜单模块 30 | // @Accept json 31 | // @Produce json 32 | // @Security Bearer 33 | // @Param request body CreateRequest true "params" 34 | // @Success 200 {object} api.Response 35 | // @Router /menu [post] 36 | func (m *Handler) StoreMenu(ctx *gin.Context) { 37 | req := new(CreateRequest) 38 | if err := ctx.ShouldBindJSON(req); err != nil { 39 | api.Error(ctx, http.StatusBadRequest, err) 40 | return 41 | } 42 | if err := m.svc.CreateMenu(ctx, req); err != nil { 43 | m.log.WithContext(ctx).Error("svc.CreateMenu error", zap.Error(err)) 44 | api.Error(ctx, http.StatusInternalServerError, err) 45 | return 46 | } 47 | api.Success(ctx, nil) 48 | } 49 | 50 | // GetMenu godoc 51 | // @Summary 菜单列表 52 | // @Schemes 53 | // @Description 查询菜单列表 54 | // @Tags 菜单模块 55 | // @Accept json 56 | // @Produce json 57 | // @Security Bearer 58 | // @Param request query FindRequest true "params" 59 | // @Success 200 {object} Response 60 | // @Router /menu [get] 61 | func (m *Handler) GetMenu(ctx *gin.Context) { 62 | req := new(FindRequest) 63 | if err := ctx.ShouldBindQuery(req); err != nil { 64 | m.log.WithContext(ctx).Error("MenuHandler.GetMenu error", zap.Error(err)) 65 | api.Error(ctx, http.StatusBadRequest, err) 66 | return 67 | } 68 | if resp, err := m.svc.SearchMenu(ctx, req); err != nil { 69 | m.log.WithContext(ctx).Error("svc.SearchMenu error", zap.Error(err)) 70 | api.Error(ctx, http.StatusInternalServerError, err) 71 | } else { 72 | api.Success(ctx, resp) 73 | } 74 | } 75 | 76 | // GetOption godoc 77 | // @Summary 菜单选项 78 | // @Schemes 79 | // @Description 菜单下拉选项 80 | // @Tags 菜单模块 81 | // @Accept json 82 | // @Produce json 83 | // @Security Bearer 84 | // @Param request query OptionRequest true "params" 85 | // @Success 200 {object} []Option 86 | // @Router /menu/option [get] 87 | func (m *Handler) GetOption(ctx *gin.Context) { 88 | req := new(OptionRequest) 89 | if err := ctx.ShouldBindQuery(req); err != nil { 90 | m.log.WithContext(ctx).Error("MenuHandler.Options error", zap.Error(err)) 91 | api.Error(ctx, http.StatusBadRequest, err) 92 | return 93 | } 94 | if resp, err := m.svc.Options(ctx, req); err != nil { 95 | m.log.WithContext(ctx).Error("svc.Options error", zap.Error(err)) 96 | api.Error(ctx, http.StatusInternalServerError, err) 97 | } else { 98 | api.Success(ctx, resp) 99 | } 100 | } 101 | 102 | // ShowMenu godoc 103 | // @Summary 查询菜单 104 | // @Schemes 105 | // @Description 查询单个菜单信息 106 | // @Tags 菜单模块 107 | // @Accept json 108 | // @Produce json 109 | // @Security Bearer 110 | // @Param id path int true "菜单ID" 111 | // @Success 200 {object} api.Response 112 | // @Router /menu/{id} [get] 113 | func (m *Handler) ShowMenu(ctx *gin.Context) { 114 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 115 | if menu, err := m.svc.GetMenuById(ctx, id); err != nil { 116 | m.log.WithContext(ctx).Error("svc.GetMenuById error", zap.Error(err)) 117 | api.Error(ctx, http.StatusInternalServerError, err) 118 | return 119 | } else { 120 | api.Success(ctx, menu) 121 | } 122 | } 123 | 124 | // UpdateMenu godoc 125 | // @Summary 修改菜单 126 | // @Schemes 127 | // @Description 修改单个菜单信息 128 | // @Tags 菜单模块 129 | // @Accept json 130 | // @Produce json 131 | // @Security Bearer 132 | // @Param id path int true "菜单ID" 133 | // @Param request body UpdateRequest true "params" 134 | // @Success 200 {object} api.Response 135 | // @Router /menu/{id} [put] 136 | func (m *Handler) UpdateMenu(ctx *gin.Context) { 137 | req := new(UpdateRequest) 138 | if err := ctx.ShouldBindJSON(req); err != nil { 139 | m.log.WithContext(ctx).Error("MenuHandler.ShowMenu error", zap.Error(err)) 140 | api.Error(ctx, http.StatusBadRequest, err) 141 | return 142 | } 143 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 144 | if err := m.svc.UpdateMenu(ctx, id, req); err != nil { 145 | m.log.WithContext(ctx).Error("svc.UpdateMenu error", zap.Error(err)) 146 | api.Error(ctx, http.StatusInternalServerError, err) 147 | return 148 | } 149 | api.Success(ctx, nil) 150 | } 151 | 152 | // DeleteMenu godoc 153 | // @Summary 删除菜单 154 | // @Schemes 155 | // @Description 删除单个菜单 156 | // @Tags 菜单模块 157 | // @Accept json 158 | // @Produce json 159 | // @Security Bearer 160 | // @Param id path int true "菜单ID" 161 | // @Success 200 {object} api.Response 162 | // @Router /menu/{id} [delete] 163 | func (m *Handler) DeleteMenu(ctx *gin.Context) { 164 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 165 | if err := m.svc.DeleteMenu(ctx, id); err != nil { 166 | m.log.WithContext(ctx).Error("svc.DeleteMenu error", zap.Error(err)) 167 | api.Error(ctx, http.StatusInternalServerError, err) 168 | return 169 | } 170 | api.Success(ctx, nil) 171 | } 172 | -------------------------------------------------------------------------------- /internal/menu/model.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Model struct { 8 | ID uint `json:"id" gorm:"primaryKey"` 9 | Name string `json:"name" gorm:"type:varchar(128);not null;default:'';comment:菜单名称"` 10 | Title string `json:"title" gorm:"type:varchar(128);not null;default:'';comment:菜单标题"` 11 | Icon string `json:"icon" gorm:"type:varchar(128);not null;default:'';comment:菜单图标"` 12 | Path string `json:"path" gorm:"type:varchar(255);not null;default:'';comment:菜单路径"` 13 | Type string `json:"type" gorm:"type:char(1);not null;default:'';comment:菜单类型 目录D 菜单M 按钮B"` 14 | ParentId uint `json:"parent_id" gorm:"type:int;default:0;comment:上级菜单ID"` 15 | Component string `json:"component" gorm:"type:varchar(255);not null;default:'';comment:组件路径"` 16 | Sort int `json:"sort" gorm:"type:int; default:0;comment:排序值,值越大越靠前"` 17 | Visible string `json:"visible" gorm:"type:char(1);not null;default:'';comment:是否可见,Y可见 N不可见"` 18 | CreatedAt time.Time `json:"created_at" gorm:"default:null;comment:创建于"` 19 | UpdatedAt time.Time `json:"updated_at" gorm:"default:null;comment:更新于"` 20 | } 21 | 22 | func (u *Model) TableName() string { 23 | return "menu" 24 | } 25 | -------------------------------------------------------------------------------- /internal/menu/repository.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "gorm.io/gorm" 8 | "helloadmin/internal/ecode" 9 | "helloadmin/internal/repository" 10 | ) 11 | 12 | type Repository interface { 13 | Find(ctx context.Context, request *FindRequest) (int64, *[]Model, error) 14 | FindByType(ctx context.Context, typ string) (*[]Model, error) 15 | GetById(ctx context.Context, id int64) (*Model, error) 16 | GetByParentId(ctx context.Context, id int64) (*[]Model, error) 17 | Create(ctx context.Context, menu *Model) error 18 | Update(ctx context.Context, id int64, menu *Model) error 19 | Delete(ctx context.Context, id int64) error 20 | } 21 | 22 | func NewRepository(r *repository.Repository) Repository { 23 | return &menuRepository{ 24 | Repository: r, 25 | } 26 | } 27 | 28 | type menuRepository struct { 29 | *repository.Repository 30 | } 31 | 32 | func (r *menuRepository) FindByType(ctx context.Context, typ string) (*[]Model, error) { 33 | var menu []Model 34 | if err := r.DB(ctx).Where("type = ?", typ).Find(&menu).Error; err != nil { 35 | return nil, err 36 | } 37 | return &menu, nil 38 | } 39 | 40 | func (r *menuRepository) Find(ctx context.Context, req *FindRequest) (int64, *[]Model, error) { 41 | var count int64 42 | var menu []Model 43 | query := r.DB(ctx) 44 | if req.Name != "" { 45 | query = query.Where("name like ?", "%"+req.Name+"%") 46 | } 47 | if req.Visible != "" { 48 | query = query.Where("visible = ?", req.Visible) 49 | } 50 | query.Model(&menu).Count(&count) 51 | if err := query.Order("sort asc").Find(&menu).Error; err != nil { 52 | return 0, nil, err 53 | } 54 | return count, &menu, nil 55 | } 56 | 57 | func (r *menuRepository) GetByParentId(ctx context.Context, id int64) (*[]Model, error) { 58 | var menu []Model 59 | query := r.DB(ctx) 60 | if err := query.Where("parent_id = ?", id).Find(&menu).Error; err != nil { 61 | return nil, err 62 | } 63 | return &menu, nil 64 | } 65 | 66 | func (r *menuRepository) GetById(ctx context.Context, id int64) (*Model, error) { 67 | var menu Model 68 | if err := r.DB(ctx).Where("id = ?", id).First(&menu).Error; err != nil { 69 | if errors.Is(err, gorm.ErrRecordNotFound) { 70 | return nil, ecode.ErrNotFound 71 | } 72 | return nil, err 73 | } 74 | return &menu, nil 75 | } 76 | 77 | func (r *menuRepository) Create(ctx context.Context, menu *Model) error { 78 | if err := r.DB(ctx).Create(menu).Error; err != nil { 79 | return err 80 | } 81 | return nil 82 | } 83 | 84 | func (r *menuRepository) Update(ctx context.Context, id int64, menu *Model) error { 85 | menu.ID = uint(id) 86 | if err := r.DB(ctx).Model(menu).Omit("updated_at").Updates(menu).Error; err != nil { 87 | return err 88 | } 89 | return nil 90 | } 91 | 92 | func (r *menuRepository) Delete(ctx context.Context, id int64) error { 93 | if err := r.DB(ctx).Delete(&Model{}, id).Error; err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /internal/menu/service.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "helloadmin/internal/ecode" 8 | ) 9 | 10 | type Service interface { 11 | GetMenuById(ctx context.Context, id int64) (*ResponseItem, error) 12 | SearchMenu(ctx context.Context, request *FindRequest) (Response, error) 13 | CreateMenu(ctx context.Context, request *CreateRequest) error 14 | UpdateMenu(ctx context.Context, id int64, request *UpdateRequest) error 15 | DeleteMenu(ctx context.Context, id int64) error 16 | Options(ctx context.Context, req *OptionRequest) ([]Option, error) 17 | } 18 | 19 | func NewService(repo Repository) Service { 20 | return &service{ 21 | repo: repo, 22 | } 23 | } 24 | 25 | type service struct { 26 | repo Repository 27 | } 28 | 29 | func (s *service) GetMenuById(ctx context.Context, id int64) (*ResponseItem, error) { 30 | menu, err := s.repo.GetById(ctx, id) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return &ResponseItem{ 35 | ID: menu.ID, 36 | Name: menu.Name, 37 | Title: menu.Title, 38 | Icon: menu.Icon, 39 | Path: menu.Path, 40 | Type: menu.Type, 41 | ParentId: menu.ParentId, 42 | Component: menu.Component, 43 | Sort: menu.Sort, 44 | Visible: menu.Visible, 45 | CreatedAt: menu.CreatedAt.Format(time.DateTime), 46 | UpdatedAt: menu.UpdatedAt.Format(time.DateTime), 47 | }, nil 48 | } 49 | 50 | func (s *service) SearchMenu(ctx context.Context, req *FindRequest) (resp Response, err error) { 51 | resp.Items = make([]ResponseItem, 0) 52 | count, menuList, err := s.repo.Find(ctx, req) 53 | if err != nil { 54 | return resp, err 55 | } 56 | if count > 0 { 57 | // Convert the flat menu list to a tree structure 58 | resp.Items = buildMenuTree(menuList, 0) 59 | } 60 | return resp, nil 61 | } 62 | 63 | func (s *service) Options(ctx context.Context, req *OptionRequest) ([]Option, error) { 64 | var menus *[]Model 65 | var err error 66 | switch req.Type { 67 | case TypeDirectory: 68 | return []Option{{Label: "顶级", Value: 0}}, nil 69 | case TypeMenu: 70 | menus, err = s.repo.FindByType(ctx, TypeDirectory) 71 | return buildOptions(*menus, true) 72 | case TypeButton: 73 | menus, err = s.repo.FindByType(ctx, TypeMenu) 74 | return buildOptions(*menus, false) 75 | default: 76 | return nil, err 77 | } 78 | } 79 | 80 | func buildOptions(menus []Model, top bool) ([]Option, error) { 81 | options := make([]Option, 1) 82 | if top { 83 | options[0] = Option{Label: "顶级", Value: 0} 84 | } 85 | if len(menus) > 0 { 86 | for _, item := range menus { 87 | options = append(options, Option{ 88 | Label: item.Title, 89 | Value: item.ID, 90 | }) 91 | } 92 | return options, nil 93 | } 94 | return options, nil 95 | } 96 | 97 | func buildMenuTree(menuList *[]Model, parentId uint) []ResponseItem { 98 | result := make([]ResponseItem, 0) 99 | for _, menuItem := range *menuList { 100 | if menuItem.ParentId == parentId { 101 | child := ResponseItem{ 102 | ID: menuItem.ID, 103 | Name: menuItem.Name, 104 | Title: menuItem.Title, 105 | Icon: menuItem.Icon, 106 | Path: menuItem.Path, 107 | Type: menuItem.Type, 108 | ParentId: menuItem.ParentId, 109 | Component: menuItem.Component, 110 | Sort: menuItem.Sort, 111 | Visible: menuItem.Visible, 112 | CreatedAt: menuItem.CreatedAt.Format(time.DateTime), 113 | UpdatedAt: menuItem.UpdatedAt.Format(time.DateTime), 114 | } 115 | // Recursively build the tree for child items 116 | child.Children = buildMenuTree(menuList, menuItem.ID) 117 | result = append(result, child) 118 | } 119 | } 120 | return result 121 | } 122 | 123 | func (s *service) CreateMenu(ctx context.Context, req *CreateRequest) error { 124 | if req.ParentId > 0 { 125 | if menu, _ := s.repo.GetById(ctx, int64(req.ParentId)); menu == nil { 126 | return ecode.ErrMenuParentedNotFound 127 | } 128 | } 129 | menu := Model{ 130 | Name: req.Name, 131 | Title: req.Title, 132 | Icon: req.Icon, 133 | Path: req.Path, 134 | Type: req.Type, 135 | ParentId: req.ParentId, 136 | Component: req.Component, 137 | Sort: req.Sort, 138 | Visible: req.Visible, 139 | CreatedAt: time.Now(), 140 | UpdatedAt: time.Now(), 141 | } 142 | return s.repo.Create(ctx, &menu) 143 | } 144 | 145 | func (s *service) UpdateMenu(ctx context.Context, id int64, req *UpdateRequest) error { 146 | menu, err := s.repo.GetById(ctx, id) 147 | if err != nil { 148 | return err 149 | } 150 | menu.Name = req.Name 151 | menu.Title = req.Title 152 | menu.Icon = req.Icon 153 | menu.Path = req.Path 154 | menu.Type = req.Type 155 | menu.ParentId = req.ParentId 156 | menu.Component = req.Component 157 | menu.Sort = req.Sort 158 | menu.Visible = req.Visible 159 | menu.UpdatedAt = time.Now() 160 | return s.repo.Update(ctx, id, menu) 161 | } 162 | 163 | func (s *service) DeleteMenu(ctx context.Context, id int64) error { 164 | menu, err := s.repo.GetByParentId(ctx, id) 165 | if err != nil { 166 | return err 167 | } 168 | // 删除菜单时,判断是否存在下级 169 | if len(*menu) > 0 { 170 | return ecode.ErrMenuHasChild 171 | } 172 | return s.repo.Delete(ctx, id) 173 | } 174 | -------------------------------------------------------------------------------- /internal/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func CORSMiddleware() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | method := c.Request.Method 12 | c.Header("Access-Control-Allow-Origin", c.GetHeader("Origin")) 13 | c.Header("Access-Control-Allow-Credentials", "true") 14 | 15 | if method == "OPTIONS" { 16 | c.Header("Access-Control-Allow-Methods", c.GetHeader("Access-Control-Request-Method")) 17 | c.Header("Access-Control-Allow-Headers", c.GetHeader("Access-Control-Request-Headers")) 18 | c.Header("Access-Control-Max-Age", "7200") 19 | c.AbortWithStatus(http.StatusNoContent) 20 | return 21 | } 22 | c.Next() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "go.uber.org/zap" 8 | "helloadmin/internal/api" 9 | "helloadmin/internal/ecode" 10 | "helloadmin/pkg/jwt" 11 | "helloadmin/pkg/log" 12 | ) 13 | 14 | func StrictAuth(j *jwt.JWT, logger *log.Logger) gin.HandlerFunc { 15 | return func(ctx *gin.Context) { 16 | tokenString := ctx.Request.Header.Get("Authorization") 17 | if tokenString == "" { 18 | logger.WithContext(ctx).Warn("No token", zap.Any("data", map[string]interface{}{ 19 | "url": ctx.Request.URL, 20 | "params": ctx.Params, 21 | })) 22 | api.Error(ctx, http.StatusUnauthorized, ecode.ErrUnauthorized) 23 | ctx.Abort() 24 | return 25 | } 26 | 27 | claims, err := j.ParseToken(tokenString) 28 | if err != nil { 29 | logger.WithContext(ctx).Error("token error", zap.Any("data", map[string]interface{}{ 30 | "url": ctx.Request.URL, 31 | "params": ctx.Params, 32 | }), zap.Error(err)) 33 | api.Error(ctx, http.StatusUnauthorized, ecode.ErrUnauthorized) 34 | ctx.Abort() 35 | return 36 | } 37 | 38 | ctx.Set("claims", claims) 39 | recoveryLoggerFunc(ctx, logger) 40 | ctx.Next() 41 | } 42 | } 43 | 44 | func NoStrictAuth(j *jwt.JWT, logger *log.Logger) gin.HandlerFunc { 45 | return func(ctx *gin.Context) { 46 | tokenString := ctx.Request.Header.Get("Authorization") 47 | if tokenString == "" { 48 | tokenString, _ = ctx.Cookie("accessToken") 49 | } 50 | if tokenString == "" { 51 | tokenString = ctx.Query("accessToken") 52 | } 53 | if tokenString == "" { 54 | ctx.Next() 55 | return 56 | } 57 | 58 | claims, err := j.ParseToken(tokenString) 59 | if err != nil { 60 | ctx.Next() 61 | return 62 | } 63 | 64 | ctx.Set("claims", claims) 65 | recoveryLoggerFunc(ctx, logger) 66 | ctx.Next() 67 | } 68 | } 69 | 70 | func recoveryLoggerFunc(ctx *gin.Context, logger *log.Logger) { 71 | if userInfo, ok := ctx.MustGet("claims").(*jwt.MyCustomClaims); ok { 72 | logger.WithValue(ctx, zap.String("UserId", userInfo.UserId)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/middleware/log.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | "go.uber.org/zap" 10 | "helloadmin/pkg/helper/md5" 11 | "helloadmin/pkg/helper/uuid" 12 | "helloadmin/pkg/log" 13 | ) 14 | 15 | func RequestLogMiddleware(logger *log.Logger) gin.HandlerFunc { 16 | return func(ctx *gin.Context) { 17 | // The configuration is initialized once per request 18 | trace := md5.Md5(uuid.GenUUID()) 19 | logger.WithValue(ctx, zap.String("trace", trace)) 20 | logger.WithValue(ctx, zap.String("request_method", ctx.Request.Method)) 21 | logger.WithValue(ctx, zap.Any("request_headers", ctx.Request.Header)) 22 | logger.WithValue(ctx, zap.String("request_url", ctx.Request.URL.String())) 23 | if ctx.Request.Body != nil { 24 | bodyBytes, _ := ctx.GetRawData() 25 | ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // 关键点 26 | logger.WithValue(ctx, zap.String("request_params", string(bodyBytes))) 27 | } 28 | logger.WithContext(ctx).Info("Request") 29 | ctx.Next() 30 | } 31 | } 32 | 33 | func ResponseLogMiddleware(logger *log.Logger) gin.HandlerFunc { 34 | return func(ctx *gin.Context) { 35 | blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: ctx.Writer} 36 | ctx.Writer = blw 37 | startTime := time.Now() 38 | ctx.Next() 39 | duration := time.Since(startTime).String() 40 | ctx.Header("X-Response-Time", duration) 41 | logger.WithContext(ctx).Info("Response", zap.Any("response_body", blw.body.String()), zap.Any("time", duration)) 42 | } 43 | } 44 | 45 | type bodyLogWriter struct { 46 | gin.ResponseWriter 47 | body *bytes.Buffer 48 | } 49 | 50 | func (w bodyLogWriter) Write(b []byte) (int, error) { 51 | w.body.Write(b) 52 | return w.ResponseWriter.Write(b) 53 | } 54 | -------------------------------------------------------------------------------- /internal/middleware/sign.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/spf13/viper" 10 | "helloadmin/internal/api" 11 | "helloadmin/internal/ecode" 12 | "helloadmin/pkg/helper/md5" 13 | "helloadmin/pkg/log" 14 | ) 15 | 16 | func SignMiddleware(logger *log.Logger, conf *viper.Viper) gin.HandlerFunc { 17 | return func(ctx *gin.Context) { 18 | requiredHeaders := []string{"Timestamp", "Nonce", "Sign", "App-Version"} 19 | 20 | for _, header := range requiredHeaders { 21 | value, ok := ctx.Request.Header[header] 22 | if !ok || len(value) == 0 { 23 | api.Error(ctx, http.StatusBadRequest, ecode.ErrBadRequest) 24 | ctx.Abort() 25 | return 26 | } 27 | } 28 | 29 | data := map[string]string{ 30 | "AppKey": conf.GetString("security.api_sign.app_key"), 31 | "Timestamp": ctx.Request.Header.Get("Timestamp"), 32 | "Nonce": ctx.Request.Header.Get("Nonce"), 33 | "AppVersion": ctx.Request.Header.Get("App-Version"), 34 | } 35 | 36 | var keys []string 37 | for k := range data { 38 | keys = append(keys, k) 39 | } 40 | sort.Slice(keys, func(i, j int) bool { return strings.ToLower(keys[i]) < strings.ToLower(keys[j]) }) 41 | 42 | var str string 43 | for _, k := range keys { 44 | str += k + data[k] 45 | } 46 | str += conf.GetString("security.api_sign.app_security") 47 | 48 | if ctx.Request.Header.Get("Sign") != strings.ToUpper(md5.Md5(str)) { 49 | api.Error(ctx, http.StatusBadRequest, ecode.ErrBadRequest) 50 | ctx.Abort() 51 | return 52 | } 53 | ctx.Next() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/operation_record/model.go: -------------------------------------------------------------------------------- 1 | package operation_record 2 | 3 | import "time" 4 | 5 | type Model struct { 6 | ID uint `json:"id" gorm:"primaryKey"` 7 | UserId string `json:"userId" gorm:"type:varchar(64);not null;default:'';index:idx_user_id;unique;comment:账号唯一ID"` 8 | Operation string `json:"operation" gorm:"type:varchar(64);not null;default:'';comment:操作类型"` 9 | Path string `json:"path" gorm:"type:varchar(255);not null;default:'';comment:操作路径"` 10 | Method string `json:"method" gorm:"type:varchar(128);not null;default:'';comment:请求方式"` 11 | Ip string `json:"ip" gorm:"type:varchar(60);not null;default:'';comment:客户端IP"` 12 | HttpStatus int `json:"httpStatus" gorm:"type:int;not null;default:0;comment:HTTP状态码"` 13 | Payload string `json:"payload" gorm:"type:text;comment:请求参数"` 14 | Response string `json:"response" gorm:"type:text;comment:响应结果"` 15 | CreatedAt time.Time `json:"createdAt,omitempty" gorm:"default:null;comment:创建于"` 16 | UpdatedAt time.Time `json:"updatedAt,omitempty" gorm:"default:null;comment:更新于"` 17 | } 18 | 19 | func (m *Model) TableName() string { 20 | return "operation_record" 21 | } 22 | -------------------------------------------------------------------------------- /internal/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/redis/go-redis/v9" 9 | "github.com/spf13/viper" 10 | "gorm.io/driver/mysql" 11 | "gorm.io/gorm" 12 | "helloadmin/pkg/log" 13 | "moul.io/zapgorm2" 14 | ) 15 | 16 | const ctxTxKey = "TxKey" 17 | 18 | type Repository struct { 19 | db *gorm.DB 20 | rdb *redis.Client 21 | logger *log.Logger 22 | } 23 | 24 | func NewRepository(db *gorm.DB, rdb *redis.Client, logger *log.Logger) *Repository { 25 | return &Repository{ 26 | db: db, 27 | rdb: rdb, 28 | logger: logger, 29 | } 30 | } 31 | 32 | type Transaction interface { 33 | Transaction(ctx context.Context, fn func(ctx context.Context) error) error 34 | } 35 | 36 | func NewTransaction(r *Repository) Transaction { 37 | return r 38 | } 39 | 40 | // DB return tx 41 | // If you need to create a Transaction, you must call DB(ctx) and Transaction(ctx,fn) 42 | func (r *Repository) DB(ctx context.Context) *gorm.DB { 43 | v := ctx.Value(ctxTxKey) 44 | if v != nil { 45 | if tx, ok := v.(*gorm.DB); ok { 46 | return tx 47 | } 48 | } 49 | return r.db.WithContext(ctx) 50 | } 51 | 52 | func (r *Repository) Transaction(ctx context.Context, fn func(ctx context.Context) error) error { 53 | return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { 54 | ctx = context.WithValue(ctx, ctxTxKey, tx) 55 | return fn(ctx) 56 | }) 57 | } 58 | 59 | func NewDB(conf *viper.Viper, l *log.Logger) *gorm.DB { 60 | logger := zapgorm2.New(l.Logger) 61 | logger.SetAsDefault() 62 | db, err := gorm.Open(mysql.Open(conf.GetString("data.mysql.user")), &gorm.Config{Logger: logger}) 63 | if err != nil { 64 | panic(err) 65 | } 66 | db = db.Debug() 67 | return db 68 | } 69 | 70 | func NewRedis(conf *viper.Viper) *redis.Client { 71 | rdb := redis.NewClient(&redis.Options{ 72 | Addr: conf.GetString("data.redis.addr"), 73 | Password: conf.GetString("data.redis.password"), 74 | DB: conf.GetInt("data.redis.db"), 75 | }) 76 | 77 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 78 | defer cancel() 79 | 80 | _, err := rdb.Ping(ctx).Result() 81 | if err != nil { 82 | panic(fmt.Sprintf("redis error: %s", err.Error())) 83 | } 84 | 85 | return rdb 86 | } 87 | -------------------------------------------------------------------------------- /internal/role/api_types.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | type CreateRequest struct { 4 | Name string `json:"name" binding:"required,min=1,max=50" example:"test"` // 角色名称 5 | Describe string `json:"describe" binding:"max=255" example:"this is describe"` // 角色描述 6 | } 7 | 8 | type UpdateRequest struct { 9 | Name string `json:"name" binding:"required,min=1,max=50" example:"test"` 10 | Describe string `json:"describe" binding:"max=255" example:"this is describe"` 11 | } 12 | 13 | type FindRequest struct { 14 | Name string `form:"name" binding:"max=50" example:"test"` // 角色名称 15 | } 16 | 17 | type MenuRequest struct { 18 | MenuId []uint `json:"menuId" binding:"required,unique,dive,gt=0"` // 菜单ID 19 | } 20 | 21 | type Response struct { 22 | Items []ResponseItem `json:"items"` 23 | } 24 | 25 | type ResponseItem struct { 26 | Id uint `json:"id"` 27 | Name string `json:"name"` 28 | Describe string `json:"describe"` 29 | CreatedAt string `json:"createdAt"` 30 | UpdatedAt string `json:"updatedAt"` 31 | MenuId []uint `json:"menuId"` 32 | } 33 | 34 | type MenuItem struct { 35 | ID uint `json:"id"` 36 | Name string `json:"name"` 37 | Title string `json:"title"` 38 | Icon string `json:"icon"` 39 | Path string `json:"path"` 40 | Type string `json:"type"` 41 | ParentId uint `json:"parentId"` 42 | Component string `json:"component"` 43 | Sort int `json:"sort"` 44 | Visible string `json:"visible"` 45 | CreatedAt string `json:"createdAt"` 46 | UpdatedAt string `json:"updatedAt"` 47 | } 48 | -------------------------------------------------------------------------------- /internal/role/handler.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "go.uber.org/zap" 9 | "helloadmin/internal/api" 10 | "helloadmin/internal/ecode" 11 | "helloadmin/pkg/log" 12 | ) 13 | 14 | type Handler struct { 15 | log *log.Logger 16 | svc Service 17 | } 18 | 19 | func NewHandler(log *log.Logger, svc Service) *Handler { 20 | return &Handler{ 21 | log: log, 22 | svc: svc, 23 | } 24 | } 25 | 26 | // StoreRole godoc 27 | // @Summary 创建角色 28 | // @Schemes 29 | // @Description 创建角色 30 | // @Tags 角色模块 31 | // @Accept json 32 | // @Produce json 33 | // @Security Bearer 34 | // @Param request body CreateRequest true "params" 35 | // @Success 200 {object} api.Response 36 | // @Router /role [post] 37 | func (r *Handler) StoreRole(ctx *gin.Context) { 38 | req := new(CreateRequest) 39 | if err := ctx.ShouldBindJSON(req); err != nil { 40 | r.log.WithContext(ctx).Error("RoleHandler.StoreRole ShouldBindJSON error", zap.Error(err)) 41 | api.Error(ctx, http.StatusBadRequest, err) 42 | return 43 | } 44 | if err := r.svc.CreateRole(ctx, req); err != nil { 45 | r.log.WithContext(ctx).Error("svc.CreateRole error", zap.Error(err)) 46 | api.Error(ctx, http.StatusInternalServerError, err) 47 | return 48 | } 49 | api.Success(ctx, nil) 50 | } 51 | 52 | // GetRole godoc 53 | // @Summary 角色列表 54 | // @Schemes 55 | // @Description 查询角色列表 56 | // @Tags 角色模块 57 | // @Accept json 58 | // @Produce json 59 | // @Security Bearer 60 | // @Param request query FindRequest true "params" 61 | // @Success 200 {object} api.Response 62 | // @Router /role [get] 63 | func (r *Handler) GetRole(ctx *gin.Context) { 64 | req := new(FindRequest) 65 | if err := ctx.ShouldBindQuery(req); err != nil { 66 | r.log.WithContext(ctx).Error("RoleHandler.GetRole error", zap.Error(err)) 67 | api.Error(ctx, http.StatusBadRequest, err) 68 | return 69 | } 70 | role, err := r.svc.SearchRole(ctx, req) 71 | if err != nil { 72 | r.log.WithContext(ctx).Error("svc.SearchRole error", zap.Error(err)) 73 | return 74 | } 75 | api.Success(ctx, role) 76 | } 77 | 78 | // ShowRole godoc 79 | // @Summary 查询角色 80 | // @Schemes 81 | // @Description 查询单个角色信息 82 | // @Tags 角色模块 83 | // @Accept json 84 | // @Produce json 85 | // @Security Bearer 86 | // @Param id path int true "角色ID" 87 | // @Success 200 {object} api.Response 88 | // @Router /role/{id} [get] 89 | func (r *Handler) ShowRole(ctx *gin.Context) { 90 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 91 | if role, err := r.svc.GetRoleById(ctx, id); err != nil { 92 | r.log.WithContext(ctx).Error("svc.GetRoleById error", zap.Error(err)) 93 | api.Error(ctx, http.StatusInternalServerError, err) 94 | return 95 | } else { 96 | api.Success(ctx, role) 97 | } 98 | } 99 | 100 | // UpdateRole godoc 101 | // @Summary 修改角色 102 | // @Schemes 103 | // @Description 修改单个角色信息 104 | // @Tags 角色模块 105 | // @Accept json 106 | // @Produce json 107 | // @Security Bearer 108 | // @Param id path int true "角色ID" 109 | // @Param request body UpdateRequest true "params" 110 | // @Success 200 {object} api.Response 111 | // @Router /role/{id} [put] 112 | func (r *Handler) UpdateRole(ctx *gin.Context) { 113 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 114 | req := new(UpdateRequest) 115 | if err := ctx.ShouldBindJSON(req); err != nil { 116 | r.log.WithContext(ctx).Error("RoleHandler.ShowRole error", zap.Error(err)) 117 | api.Error(ctx, http.StatusBadRequest, ecode.ErrBadRequest) 118 | return 119 | } 120 | if err := r.svc.UpdateRole(ctx, id, req); err != nil { 121 | r.log.WithContext(ctx).Error("svc.UpdateRole error", zap.Error(err)) 122 | api.Error(ctx, http.StatusInternalServerError, err) 123 | return 124 | } 125 | api.Success(ctx, nil) 126 | } 127 | 128 | // UpdateRoleMenu godoc 129 | // @Summary 修改角色权限 130 | // @Schemes 131 | // @Description 修改单个角色权限 132 | // @Tags 角色模块 133 | // @Accept json 134 | // @Produce json 135 | // @Security Bearer 136 | // @Param id path int true "角色ID" 137 | // @Param request body MenuRequest true "params" 138 | // @Success 200 {object} api.Response 139 | // @Router /role/{id}/menu [put] 140 | func (r *Handler) UpdateRoleMenu(ctx *gin.Context) { 141 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 142 | req := new(MenuRequest) 143 | if err := ctx.ShouldBindJSON(req); err != nil { 144 | r.log.WithContext(ctx).Error("RoleHandler.ShowRole error", zap.Error(err)) 145 | api.Error(ctx, http.StatusBadRequest, err) 146 | return 147 | } 148 | if err := r.svc.UpdateRoleMenu(ctx, id, req); err != nil { 149 | r.log.WithContext(ctx).Error("svc.UpdateRole error", zap.Error(err)) 150 | api.Error(ctx, http.StatusInternalServerError, err) 151 | return 152 | } 153 | api.Success(ctx, nil) 154 | } 155 | 156 | // DeleteRole godoc 157 | // @Summary 删除角色 158 | // @Schemes 159 | // @Description 删除单个角色 160 | // @Tags 角色模块 161 | // @Accept json 162 | // @Produce json 163 | // @Security Bearer 164 | // @Param id path int true "角色ID" 165 | // @Success 200 {object} api.Response 166 | // @Router /role/{id} [delete] 167 | func (r *Handler) DeleteRole(ctx *gin.Context) { 168 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 169 | if err := r.svc.DeleteRole(ctx, id); err != nil { 170 | r.log.WithContext(ctx).Error("svc.DeleteRole error", zap.Error(err)) 171 | api.Error(ctx, http.StatusInternalServerError, err) 172 | return 173 | } 174 | api.Success(ctx, nil) 175 | } 176 | -------------------------------------------------------------------------------- /internal/role/model.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "time" 5 | 6 | "helloadmin/internal/menu" 7 | ) 8 | 9 | type Model struct { 10 | ID uint `json:"id" gorm:"primaryKey"` 11 | Name string `json:"name" gorm:"type:varchar(60);not null;default:'';comment:角色名称"` 12 | Describe string `json:"describe" gorm:"type:varchar(255);not null;default:'';comment:角色描述"` 13 | CreatedAt time.Time `json:"created_at,omitempty" gorm:"default:null;comment:创建于"` 14 | UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"default:null;comment:更新于"` 15 | Menus []menu.Model `json:"_" gorm:"many2many:role_menu"` 16 | } 17 | 18 | func (m *Model) TableName() string { 19 | return "role" 20 | } 21 | -------------------------------------------------------------------------------- /internal/role/repository.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "gorm.io/gorm" 8 | "helloadmin/internal/ecode" 9 | "helloadmin/internal/menu" 10 | "helloadmin/internal/repository" 11 | ) 12 | 13 | type Repository interface { 14 | Find(ctx context.Context, request *FindRequest) (int64, *[]Model, error) 15 | GetById(ctx context.Context, id int64) (*Model, error) 16 | Create(ctx context.Context, role *Model) error 17 | Update(ctx context.Context, id int64, role *Model) error 18 | UpdateRoleMenu(ctx context.Context, id int64, req *MenuRequest) error 19 | Delete(ctx context.Context, id int64) error 20 | } 21 | 22 | func NewRepository(r *repository.Repository) Repository { 23 | return &roleRepository{ 24 | Repository: r, 25 | } 26 | } 27 | 28 | type roleRepository struct { 29 | *repository.Repository 30 | } 31 | 32 | func (r *roleRepository) Find(ctx context.Context, req *FindRequest) (int64, *[]Model, error) { 33 | var count int64 34 | var role []Model 35 | query := r.DB(ctx) 36 | if req.Name != "" { 37 | query = query.Where("name = ?", req.Name) 38 | } 39 | query.Model(Model{}).Count(&count) 40 | if result := query.Order("id desc").Preload("Menus").Find(&role); result.Error != nil { 41 | return count, nil, result.Error 42 | } 43 | return count, &role, nil 44 | } 45 | 46 | func (r *roleRepository) GetById(ctx context.Context, id int64) (*Model, error) { 47 | var role Model 48 | if err := r.DB(ctx).Where("id = ?", id).Preload("Menus").First(&role).Error; err != nil { 49 | if errors.Is(err, gorm.ErrRecordNotFound) { 50 | return nil, ecode.ErrRoleNotFound 51 | } 52 | return nil, err 53 | } 54 | return &role, nil 55 | } 56 | 57 | func (r *roleRepository) Create(ctx context.Context, role *Model) error { 58 | if err := r.DB(ctx).Create(role).Error; err != nil { 59 | return err 60 | } 61 | return nil 62 | } 63 | 64 | func (r *roleRepository) Update(ctx context.Context, id int64, role *Model) error { 65 | if err := r.DB(ctx).Model(role).Where("id = ?", id).Updates(role).Error; err != nil { 66 | return err 67 | } 68 | return nil 69 | } 70 | 71 | func (r *roleRepository) UpdateRoleMenu(ctx context.Context, id int64, req *MenuRequest) error { 72 | // 开启数据库事务 73 | tx := r.DB(ctx).Begin() 74 | if tx.Error != nil { 75 | return tx.Error 76 | } 77 | defer func() { 78 | if r := recover(); r != nil { 79 | tx.Rollback() // 回滚事务 80 | } 81 | }() 82 | 83 | var role Model 84 | if err := tx.Preload("Menus").First(&role, id).Error; err != nil { 85 | tx.Rollback() // 回滚事务 86 | return err 87 | } 88 | 89 | // 清空角色原有的关联菜单 90 | if err := tx.Model(&role).Association("Menus").Clear(); err != nil { 91 | tx.Rollback() // 回滚事务 92 | return err 93 | } 94 | 95 | if len(req.MenuId) > 0 { 96 | var menus []menu.Model 97 | if err := tx.Where("id IN (?)", req.MenuId).Find(&menus).Error; err != nil { 98 | tx.Rollback() // 回滚事务 99 | return err 100 | } 101 | 102 | // 将新的菜单关联到角色 103 | if err := tx.Model(&role).Association("Menus").Append(menus); err != nil { 104 | tx.Rollback() // 回滚事务 105 | return err 106 | } 107 | } 108 | 109 | // 提交事务 110 | return tx.Commit().Error 111 | } 112 | 113 | func (r *roleRepository) Delete(ctx context.Context, id int64) error { 114 | var count int64 115 | r.DB(ctx).Model(Model{}).Where("role_id = ?", id).Count(&count) 116 | if count > 0 { 117 | return ecode.ErrRoleHasUser 118 | } 119 | if err := r.DB(ctx).Delete(&Model{}, id).Error; err != nil { 120 | return err 121 | } 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /internal/role/service.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type Service interface { 9 | GetRoleById(ctx context.Context, id int64) (*ResponseItem, error) 10 | GetRoleMenuById(ctx context.Context, id int64) (*[]MenuItem, error) 11 | SearchRole(ctx context.Context, request *FindRequest) (*Response, error) 12 | CreateRole(ctx context.Context, request *CreateRequest) error 13 | UpdateRole(ctx context.Context, id int64, request *UpdateRequest) error 14 | DeleteRole(ctx context.Context, id int64) error 15 | UpdateRoleMenu(ctx context.Context, id int64, request *MenuRequest) error 16 | } 17 | 18 | func NewService(repo Repository) Service { 19 | return &roleService{ 20 | repo: repo, 21 | } 22 | } 23 | 24 | type roleService struct { 25 | repo Repository 26 | } 27 | 28 | func (s *roleService) GetRoleById(ctx context.Context, id int64) (*ResponseItem, error) { 29 | role, err := s.repo.GetById(ctx, id) 30 | if err != nil { 31 | return nil, err 32 | } 33 | menuIds := make([]uint, 0) 34 | if len(role.Menus) > 0 { 35 | for _, menu := range role.Menus { 36 | menuIds = append(menuIds, menu.ID) 37 | } 38 | } 39 | return &ResponseItem{ 40 | Id: role.ID, 41 | Name: role.Name, 42 | Describe: role.Describe, 43 | UpdatedAt: role.UpdatedAt.Format(time.DateTime), 44 | CreatedAt: role.CreatedAt.Format(time.DateTime), 45 | MenuId: menuIds, 46 | }, nil 47 | } 48 | 49 | func (s *roleService) GetRoleMenuById(ctx context.Context, id int64) (*[]MenuItem, error) { 50 | role, err := s.repo.GetById(ctx, id) 51 | if err != nil { 52 | return nil, err 53 | } 54 | menuItem := make([]MenuItem, 0) 55 | for _, menu := range role.Menus { 56 | tmp := MenuItem{ 57 | ID: menu.ID, 58 | Title: menu.Title, 59 | Component: menu.Component, 60 | Visible: menu.Visible, 61 | Name: menu.Name, 62 | Icon: menu.Icon, 63 | ParentId: menu.ParentId, 64 | Path: menu.Path, 65 | Type: menu.Type, 66 | Sort: menu.Sort, 67 | CreatedAt: menu.CreatedAt.Format(time.DateTime), 68 | UpdatedAt: menu.UpdatedAt.Format(time.DateTime), 69 | } 70 | menuItem = append(menuItem, tmp) 71 | } 72 | return &menuItem, nil 73 | } 74 | 75 | func (s *roleService) SearchRole(ctx context.Context, req *FindRequest) (*Response, error) { 76 | var result Response 77 | count, roles, err := s.repo.Find(ctx, req) 78 | if err != nil { 79 | return nil, err 80 | } 81 | result.Items = make([]ResponseItem, 0) 82 | if count > 0 { 83 | for _, role := range *roles { 84 | menuIds := make([]uint, 0) 85 | if len(role.Menus) > 0 { 86 | for _, menu := range role.Menus { 87 | menuIds = append(menuIds, menu.ID) 88 | } 89 | } 90 | tmp := ResponseItem{ 91 | Id: role.ID, 92 | Name: role.Name, 93 | Describe: role.Describe, 94 | MenuId: menuIds, 95 | UpdatedAt: role.UpdatedAt.Format(time.DateTime), 96 | CreatedAt: role.CreatedAt.Format(time.DateTime), 97 | } 98 | result.Items = append(result.Items, tmp) 99 | } 100 | } 101 | return &result, nil 102 | } 103 | 104 | func (s *roleService) CreateRole(ctx context.Context, req *CreateRequest) error { 105 | role := Model{ 106 | Name: req.Name, 107 | Describe: req.Describe, 108 | CreatedAt: time.Now(), 109 | UpdatedAt: time.Now(), 110 | } 111 | return s.repo.Create(ctx, &role) 112 | } 113 | 114 | func (s *roleService) UpdateRole(ctx context.Context, id int64, req *UpdateRequest) error { 115 | role := Model{ 116 | Name: req.Name, 117 | Describe: req.Describe, 118 | UpdatedAt: time.Now(), 119 | } 120 | return s.repo.Update(ctx, id, &role) 121 | } 122 | 123 | func (s *roleService) UpdateRoleMenu(ctx context.Context, id int64, req *MenuRequest) error { 124 | return s.repo.UpdateRoleMenu(ctx, id, req) 125 | } 126 | 127 | func (s *roleService) DeleteRole(ctx context.Context, id int64) error { 128 | return s.repo.Delete(ctx, id) 129 | } 130 | -------------------------------------------------------------------------------- /internal/server/http.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/spf13/viper" 6 | swaggerfiles "github.com/swaggo/files" 7 | ginSwagger "github.com/swaggo/gin-swagger" 8 | "helloadmin/docs" 9 | "helloadmin/internal/api" 10 | "helloadmin/internal/department" 11 | "helloadmin/internal/login_record" 12 | "helloadmin/internal/menu" 13 | "helloadmin/internal/middleware" 14 | "helloadmin/internal/role" 15 | "helloadmin/internal/user" 16 | "helloadmin/pkg/jwt" 17 | "helloadmin/pkg/log" 18 | "helloadmin/pkg/server/http" 19 | ) 20 | 21 | func NewHTTPServer( 22 | logger *log.Logger, 23 | cfg *viper.Viper, 24 | jwt *jwt.JWT, 25 | userHandler *user.Handler, 26 | roleHandler *role.Handler, 27 | menuHandler *menu.Handler, 28 | departHandler *department.Handler, 29 | loginRecordHandler *login_record.Handler, 30 | ) *http.Server { 31 | gin.SetMode(gin.DebugMode) 32 | s := http.NewServer( 33 | gin.Default(), 34 | logger, 35 | http.WithServerHost(cfg.GetString("http.host")), 36 | http.WithServerPort(cfg.GetInt("http.port")), 37 | ) 38 | 39 | // swagger doc 40 | docs.SwaggerInfo.BasePath = "/api" 41 | s.GET("/swagger/*any", ginSwagger.WrapHandler( 42 | swaggerfiles.Handler, 43 | // ginSwagger.URL(fmt.Sprintf("http://localhost:%d/swagger/doc.json", cfg.GetInt("http.port"))), 44 | ginSwagger.DefaultModelsExpandDepth(-1), 45 | )) 46 | 47 | s.Use( 48 | middleware.CORSMiddleware(), 49 | middleware.ResponseLogMiddleware(logger), 50 | middleware.RequestLogMiddleware(logger), 51 | // middleware.SignMiddleware(log), 52 | ) 53 | s.GET("/", func(ctx *gin.Context) { 54 | api.Success(ctx, map[string]interface{}{ 55 | ":)": "Thank you for using HelloAdmin!", 56 | }) 57 | }) 58 | 59 | group := s.Group("/api") 60 | { 61 | // No route group has permission 62 | noAuthRouter := group.Group("/") 63 | { 64 | noAuthRouter.POST("/login", userHandler.Login) 65 | } 66 | // Strict permission routing group 67 | usr := group.Group("/user").Use(middleware.StrictAuth(jwt, logger)) 68 | { 69 | usr.GET("", userHandler.Search) 70 | usr.GET("/:id", userHandler.Show) 71 | usr.GET("/profile", userHandler.GetProfile) 72 | usr.POST("", userHandler.Store) 73 | usr.PUT("/:id", userHandler.Update) 74 | usr.DELETE("/:id", userHandler.Delete) 75 | } 76 | 77 | ror := group.Group("/role").Use(middleware.StrictAuth(jwt, logger)) 78 | { 79 | ror.GET("", roleHandler.GetRole) 80 | ror.POST("", roleHandler.StoreRole) 81 | ror.GET("/:id", roleHandler.ShowRole) 82 | ror.PUT("/:id", roleHandler.UpdateRole) 83 | ror.PUT("/:id/menu", roleHandler.UpdateRoleMenu) 84 | ror.DELETE("/:id", roleHandler.DeleteRole) 85 | } 86 | 87 | mer := group.Group("/menu").Use(middleware.StrictAuth(jwt, logger)) 88 | { 89 | mer.GET("", menuHandler.GetMenu) 90 | mer.GET("/option", menuHandler.GetOption) 91 | mer.POST("", menuHandler.StoreMenu) 92 | mer.GET("/:id", menuHandler.ShowMenu) 93 | mer.PUT("/:id", menuHandler.UpdateMenu) 94 | mer.DELETE("/:id", menuHandler.DeleteMenu) 95 | } 96 | der := group.Group("department").Use(middleware.StrictAuth(jwt, logger)) 97 | { 98 | der.GET("", departHandler.GetDepartment) 99 | der.POST("", departHandler.StoreDepartment) 100 | der.GET("/:id", departHandler.ShowDepartment) 101 | der.PUT("/:id", departHandler.UpdateDepartment) 102 | der.DELETE("/:id", departHandler.DeleteDepartment) 103 | } 104 | rer := group.Group("/record").Use(middleware.StrictAuth(jwt, logger)) 105 | { 106 | rer.GET("login", loginRecordHandler.SearchLoginRecord) 107 | rer.GET("operation", loginRecordHandler.SearchLoginRecord) 108 | } 109 | 110 | } 111 | 112 | return s 113 | } 114 | -------------------------------------------------------------------------------- /internal/server/job.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "helloadmin/pkg/log" 7 | ) 8 | 9 | type Job struct { 10 | log *log.Logger 11 | } 12 | 13 | func NewJob( 14 | log *log.Logger, 15 | ) *Job { 16 | return &Job{ 17 | log: log, 18 | } 19 | } 20 | 21 | func (j *Job) Start(ctx context.Context) error { 22 | // eg: kafka consumer 23 | return nil 24 | } 25 | 26 | func (j *Job) Stop(ctx context.Context) error { 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/server/migration.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "go.uber.org/zap" 8 | "gorm.io/gorm" 9 | "helloadmin/internal/department" 10 | login_log "helloadmin/internal/login_record" 11 | "helloadmin/internal/menu" 12 | "helloadmin/internal/operation_record" 13 | "helloadmin/internal/role" 14 | "helloadmin/internal/user" 15 | "helloadmin/pkg/log" 16 | ) 17 | 18 | type Migrate struct { 19 | db *gorm.DB 20 | log *log.Logger 21 | } 22 | 23 | func NewMigrate(db *gorm.DB, log *log.Logger) *Migrate { 24 | return &Migrate{ 25 | db: db, 26 | log: log, 27 | } 28 | } 29 | 30 | func (m *Migrate) Start(ctx context.Context) error { 31 | if err := m.db.AutoMigrate(&user.Model{}); err != nil { 32 | m.log.Error("user migrate error", zap.Error(err)) 33 | return err 34 | } 35 | if err := m.db.AutoMigrate(&role.Model{}); err != nil { 36 | m.log.Error("role migrate error", zap.Error(err)) 37 | return err 38 | } 39 | if err := m.db.AutoMigrate(&menu.Model{}); err != nil { 40 | m.log.Error("menu migrate error", zap.Error(err)) 41 | return err 42 | } 43 | if err := m.db.AutoMigrate(&department.Model{}); err != nil { 44 | m.log.Error("department migrate error", zap.Error(err)) 45 | return err 46 | } 47 | if err := m.db.AutoMigrate(&login_log.Model{}); err != nil { 48 | m.log.Error("sign_record migrate error", zap.Error(err)) 49 | return err 50 | } 51 | if err := m.db.AutoMigrate(&operation_record.Model{}); err != nil { 52 | m.log.Error("operation_record migrate error", zap.Error(err)) 53 | return err 54 | } 55 | 56 | m.log.Info("AutoMigrate success") 57 | os.Exit(0) 58 | return nil 59 | } 60 | 61 | func (m *Migrate) Stop(ctx context.Context) error { 62 | m.log.Info("AutoMigrate stop") 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/server/task.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/go-co-op/gocron" 8 | "go.uber.org/zap" 9 | "helloadmin/pkg/log" 10 | ) 11 | 12 | type Task struct { 13 | log *log.Logger 14 | scheduler *gocron.Scheduler 15 | } 16 | 17 | func NewTask(log *log.Logger) *Task { 18 | return &Task{ 19 | log: log, 20 | } 21 | } 22 | 23 | func (t *Task) Start(ctx context.Context) error { 24 | gocron.SetPanicHandler(func(jobName string, recoverData interface{}) { 25 | t.log.Error("Task Panic", zap.String("job", jobName), zap.Any("recover", recoverData)) 26 | }) 27 | 28 | // eg: crontab task 29 | t.scheduler = gocron.NewScheduler(time.UTC) 30 | // if you are in China, you will need to change the time zone as follows 31 | // t.scheduler = gocron.NewScheduler(time.FixedZone("PRC", 8*60*60)) 32 | 33 | _, err := t.scheduler.CronWithSeconds("0/3 * * * * *").Do(func() { 34 | t.log.Info("I'm a Task1.") 35 | }) 36 | if err != nil { 37 | t.log.Error("Task1 error", zap.Error(err)) 38 | } 39 | 40 | _, err = t.scheduler.Every("3s").Do(func() { 41 | t.log.Info("I'm a Task2.") 42 | }) 43 | if err != nil { 44 | t.log.Error("Task2 error", zap.Error(err)) 45 | } 46 | 47 | t.scheduler.StartBlocking() 48 | return nil 49 | } 50 | 51 | func (t *Task) Stop(ctx context.Context) error { 52 | t.scheduler.Stop() 53 | t.log.Info("Task stop...") 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/user/api_types.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "helloadmin/internal/api" 4 | 5 | type RegisterRequest struct { 6 | Email string `json:"email" binding:"required,email" example:"admin@helloadmin.com"` // 邮箱 7 | Password string `json:"password" binding:"required" example:"123456"` // 密码 8 | Nickname string `json:"nickname" binding:"required,max=50" example:"admin"` // 员工名 9 | RoleId uint `json:"roleId" example:"1"` // 角色ID 10 | DeptId uint `json:"deptId" example:"1"` // 部门ID 11 | } 12 | 13 | type FindRequest struct { 14 | Page int `form:"page" binding:"required,min=1" example:"1"` // 页码 15 | Size int `form:"size" binding:"required,min=1" example:"10"` // 每页条数 16 | Email string `form:"email" example:"admin@helloadmin.com"` // 邮箱 17 | Nickname string `form:"nickname" example:"admin"` // 员工昵称 18 | RoleId uint `form:"roleId" example:"1"` // 角色ID 19 | DeptId uint `form:"deptId" example:"1"` // 部门ID 20 | } 21 | 22 | type LoginRequest struct { 23 | Email string `json:"email" binding:"required,email" example:"admin@helloadmin.com"` // 邮箱 24 | Password string `json:"password" binding:"required" example:"123456"` // 密码 25 | } 26 | 27 | type LoginResponse struct { 28 | AccessToken string `json:"accessToken"` // 访问令牌 29 | ExpiresAt string `json:"expiresAt"` // 过期日期 30 | TokenType string `json:"tokenType"` // 令牌类型 31 | } 32 | 33 | type UpdateRequest struct { 34 | Nickname string `json:"nickname" example:"admin"` 35 | Email string `json:"email" binding:"required,email" example:"admin@helloadmin.com"` 36 | RoleId int64 `json:"roleId" example:"1"` // 角色ID 37 | DeptId int64 `json:"deptId" example:"1"` // 部门ID 38 | } 39 | 40 | type ProfileData struct { 41 | Id uint `json:"id" example:"1"` // 员工ID 42 | Email string `json:"email" example:"admin@helloadmin.com"` // 员工邮箱 43 | UserId string `json:"userId" example:"1"` // 员工编码 44 | RoleId uint `json:"roleId" example:"1"` // 员工角色ID 45 | DeptId uint `json:"deptId" example:"1"` // 员工部门ID 46 | Role struct { 47 | Id uint `json:"id"` 48 | Name string `json:"name"` 49 | } `json:"role"` 50 | Department struct { 51 | Id uint `json:"id"` 52 | Name string `json:"name"` 53 | } `json:"department"` 54 | Nickname string `json:"nickname" example:"Hi admin"` 55 | CreatedAt string `json:"createdAt" example:"2023-12-27 19:01:00"` 56 | UpdatedAt string `json:"updatedAt" example:"2023-12-27 19:01:00"` 57 | } 58 | 59 | type Response struct { 60 | Items []ProfileData `json:"items"` 61 | api.Pagination `json:"pagination"` 62 | } 63 | -------------------------------------------------------------------------------- /internal/user/handler.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "helloadmin/internal/department" 5 | "helloadmin/internal/role" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/mssola/user_agent" 11 | "go.uber.org/zap" 12 | "helloadmin/internal/api" 13 | "helloadmin/internal/ecode" 14 | logging "helloadmin/internal/login_record" 15 | "helloadmin/pkg/jwt" 16 | "helloadmin/pkg/log" 17 | ) 18 | 19 | type Handler struct { 20 | log *log.Logger 21 | us Service 22 | rs logging.Service 23 | de department.Service 24 | ro role.Service 25 | } 26 | 27 | func NewHandler(log *log.Logger, us Service, rs logging.Service, de department.Service, ro role.Service) *Handler { 28 | return &Handler{ 29 | log: log, 30 | us: us, 31 | rs: rs, 32 | de: de, 33 | ro: ro, 34 | } 35 | } 36 | 37 | // Search godoc 38 | // @Summary 搜索员工 39 | // @Schemes 40 | // @Description 搜索员工 41 | // @Tags 员工模块 42 | // @Accept json 43 | // @Produce json 44 | // @Security Bearer 45 | // @Param request query FindRequest true "params" 46 | // @Success 200 {object} api.Response 47 | // @Router /user [get] 48 | func (h *Handler) Search(ctx *gin.Context) { 49 | req := new(FindRequest) 50 | if err := ctx.ShouldBindQuery(req); err != nil { 51 | api.Error(ctx, http.StatusBadRequest, err) 52 | return 53 | } 54 | if resp, err := h.us.Search(ctx, req); err != nil { 55 | h.log.WithContext(ctx).Error("us.Search error", zap.Error(err)) 56 | api.Error(ctx, http.StatusInternalServerError, err) 57 | } else { 58 | api.Success(ctx, resp) 59 | } 60 | } 61 | 62 | // Store godoc 63 | // @Summary 添加员工 64 | // @Schemes 65 | // @Description 添加员工 66 | // @Tags 员工模块 67 | // @Accept json 68 | // @Produce json 69 | // @Security Bearer 70 | // @Param request body RegisterRequest true "params" 71 | // @Success 200 {object} api.Response 72 | // @Router /user [post] 73 | func (h *Handler) Store(ctx *gin.Context) { 74 | req := new(RegisterRequest) 75 | if err := ctx.ShouldBindJSON(req); err != nil { 76 | api.Error(ctx, http.StatusBadRequest, err) 77 | return 78 | } 79 | if err := h.us.Register(ctx, req); err != nil { 80 | h.log.WithContext(ctx).Error("us.Register error", zap.Error(err)) 81 | api.Error(ctx, http.StatusInternalServerError, err) 82 | return 83 | } 84 | api.Success(ctx, nil) 85 | } 86 | 87 | // Login godoc 88 | // @Summary 员工登录 89 | // @Schemes 90 | // @Description 91 | // @Tags 员工模块 92 | // @Accept json 93 | // @Produce json 94 | // @Param request body LoginRequest true "params" 95 | // @Success 200 {object} LoginResponse 96 | // @Router /login [post] 97 | func (h *Handler) Login(ctx *gin.Context) { 98 | var req LoginRequest 99 | if err := ctx.ShouldBindJSON(&req); err != nil { 100 | api.Error(ctx, http.StatusBadRequest, err) 101 | return 102 | } 103 | ua := user_agent.New(ctx.Request.UserAgent()) 104 | browser, _ := ua.Browser() 105 | record := logging.CreateRequest{Ip: ctx.ClientIP(), UserName: "-", Email: req.Email, Browser: browser, Platform: ua.Platform(), Os: ua.OS()} 106 | resp, err := h.us.Login(ctx, &req) 107 | if err != nil { 108 | record.ErrorMessage = err.Error() 109 | _ = h.rs.Create(ctx, &record) 110 | api.Error(ctx, http.StatusUnauthorized, err) 111 | return 112 | } 113 | _ = h.rs.Create(ctx, &record) 114 | api.Success(ctx, resp) 115 | } 116 | 117 | // Show godoc 118 | // @Summary 获取员工信息 119 | // @Schemes 120 | // @Description 121 | // @Tags 员工模块 122 | // @Accept json 123 | // @Produce json 124 | // @Security Bearer 125 | // @Success 200 {object} ProfileData 126 | // @Router /user/{id} [get] 127 | func (h *Handler) Show(ctx *gin.Context) { 128 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 129 | user, err := h.us.GetProfileById(ctx, id) 130 | if err != nil { 131 | api.Error(ctx, http.StatusBadRequest, err) 132 | return 133 | } 134 | api.Success(ctx, user) 135 | } 136 | 137 | // GetProfile godoc 138 | // @Summary 登录账号信息 139 | // @Schemes 140 | // @Description 141 | // @Tags 员工模块 142 | // @Accept json 143 | // @Produce json 144 | // @Security Bearer 145 | // @Success 200 {object} ProfileData 146 | // @Router /user/profile [get] 147 | func (h *Handler) GetProfile(ctx *gin.Context) { 148 | userId := GetUserIdFromCtx(ctx) 149 | if userId == "" { 150 | api.Error(ctx, http.StatusUnauthorized, ecode.ErrUnauthorized) 151 | return 152 | } 153 | user, err := h.us.GetProfileByUserId(ctx, userId) 154 | if err != nil { 155 | api.Error(ctx, http.StatusInternalServerError, err) 156 | return 157 | } 158 | menu, err := h.ro.GetRoleMenuById(ctx, int64(user.RoleId)) 159 | if err != nil { 160 | api.Error(ctx, http.StatusInternalServerError, err) 161 | } 162 | api.Success(ctx, map[string]interface{}{"user": user, "menu": menu}) 163 | } 164 | 165 | // Update godoc 166 | // @Summary 修改员工信息 167 | // @Schemes 168 | // @Description 169 | // @Tags 员工模块 170 | // @Accept json 171 | // @Produce json 172 | // @Security Bearer 173 | // @Param request body UpdateRequest true "params" 174 | // @Success 200 {object} api.Response 175 | // @Router /user/{id} [put] 176 | func (h *Handler) Update(ctx *gin.Context) { 177 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 178 | var req UpdateRequest 179 | if err := ctx.ShouldBindJSON(&req); err != nil { 180 | api.Error(ctx, http.StatusBadRequest, err) 181 | return 182 | } 183 | if _, err := h.de.GetDepartmentById(ctx, req.DeptId); err != nil { 184 | api.Error(ctx, http.StatusBadRequest, err) 185 | return 186 | } 187 | if err := h.us.Update(ctx, id, &req); err != nil { 188 | api.Error(ctx, http.StatusInternalServerError, err) 189 | return 190 | } 191 | api.Success(ctx, nil) 192 | } 193 | 194 | // Delete godoc 195 | // @Summary 删除员工信息 196 | // @Schemes 197 | // @Description 198 | // @Tags 员工模块 199 | // @Accept json 200 | // @Produce json 201 | // @Security Bearer 202 | // @Success 200 {object} api.Response 203 | // @Router /user/{id} [delete] 204 | func (h *Handler) Delete(ctx *gin.Context) { 205 | id, _ := strconv.ParseInt(ctx.Param("id"), 10, 64) 206 | if err := h.us.Delete(ctx, id); err != nil { 207 | api.Error(ctx, http.StatusInternalServerError, err) 208 | return 209 | } 210 | api.Success(ctx, nil) 211 | } 212 | 213 | func GetUserIdFromCtx(ctx *gin.Context) string { 214 | v, exists := ctx.Get("claims") 215 | if !exists { 216 | return "" 217 | } 218 | return v.(*jwt.MyCustomClaims).UserId 219 | } 220 | -------------------------------------------------------------------------------- /internal/user/model.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "time" 5 | 6 | "helloadmin/internal/department" 7 | "helloadmin/internal/role" 8 | 9 | "gorm.io/gorm" 10 | ) 11 | 12 | type Model struct { 13 | ID uint `json:"id" gorm:"primaryKey"` 14 | UserId string `json:"userId" gorm:"type:varchar(64);not null;default:'';index:idx_user_id;unique;comment:账号唯一ID"` 15 | Nickname string `json:"nickname" gorm:"type:varchar(64);default:'';comment:昵称"` 16 | Password string `json:"password" gorm:"type:varchar(255);not null;comment:密码"` 17 | Email string `json:"email" gorm:"type:varchar(60);not null;default:'';comment:邮箱"` 18 | Salt string `json:"salt" gorm:"type:varchar(60);not null;default:'';comment:盐字段"` 19 | RoleId uint `json:"roleId" gorm:"type:int;not null;default:0;comment:角色ID"` 20 | DeptId uint `json:"deptId" gorm:"type:int;not null;default:0;comment:部门ID"` 21 | Role role.Model `json:"_" gorm:"foreignKey:RoleId"` 22 | Department department.Model `json:"department" gorm:"foreignKey:DeptId"` 23 | CreatedAt time.Time `json:"createdAt" gorm:"default:null;comment:创建于"` 24 | UpdatedAt time.Time `json:"updatedAt" gorm:"default:null;comment:更新于"` 25 | DeletedAt gorm.DeletedAt `json:"deletedAt" gorm:"default:null;comment:删除于"` 26 | } 27 | 28 | func (u *Model) TableName() string { 29 | return "user" 30 | } 31 | -------------------------------------------------------------------------------- /internal/user/respository.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "gorm.io/gorm" 7 | "helloadmin/internal/ecode" 8 | "helloadmin/internal/repository" 9 | ) 10 | 11 | type Repository interface { 12 | Create(ctx context.Context, user *Model) error 13 | Update(ctx context.Context, user *Model) error 14 | GetById(ctx context.Context, id int64) (*Model, error) 15 | GetByUserId(ctx context.Context, id string) (*Model, error) 16 | GetByEmail(ctx context.Context, email string) (*Model, error) 17 | Search(ctx context.Context, request *FindRequest) (int64, *[]Model, error) 18 | Delete(ctx context.Context, id int64) error 19 | } 20 | 21 | func NewRepository(r *repository.Repository) Repository { 22 | return &userRepository{ 23 | Repository: r, 24 | } 25 | } 26 | 27 | type userRepository struct { 28 | *repository.Repository 29 | } 30 | 31 | func (r *userRepository) Create(ctx context.Context, user *Model) error { 32 | if err := r.DB(ctx).Create(user).Error; err != nil { 33 | return err 34 | } 35 | return nil 36 | } 37 | 38 | func (r *userRepository) Update(ctx context.Context, user *Model) error { 39 | if err := r.DB(ctx).Save(user).Error; err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | func (r *userRepository) GetById(ctx context.Context, id int64) (*Model, error) { 46 | var user Model 47 | if err := r.DB(ctx).Preload("Role").Preload("Department").Where("id = ?", id).First(&user).Error; err != nil { 48 | if errors.Is(err, gorm.ErrRecordNotFound) { 49 | return nil, ecode.ErrNotFound 50 | } 51 | return nil, err 52 | } 53 | return &user, nil 54 | } 55 | 56 | func (r *userRepository) GetByUserId(ctx context.Context, userId string) (*Model, error) { 57 | var user Model 58 | if err := r.DB(ctx).Preload("Role").Preload("Department").Where("user_id = ?", userId).First(&user).Error; err != nil { 59 | if errors.Is(err, gorm.ErrRecordNotFound) { 60 | return nil, ecode.ErrNotFound 61 | } 62 | return nil, err 63 | } 64 | return &user, nil 65 | } 66 | 67 | func (r *userRepository) GetByEmail(ctx context.Context, email string) (*Model, error) { 68 | var user Model 69 | if err := r.DB(ctx).Where("email = ?", email).First(&user).Error; err != nil { 70 | if errors.Is(err, gorm.ErrRecordNotFound) { 71 | return nil, nil 72 | } 73 | return nil, err 74 | } 75 | return &user, nil 76 | } 77 | 78 | func (r *userRepository) Search(ctx context.Context, request *FindRequest) (int64, *[]Model, error) { 79 | var ( 80 | users []Model 81 | total int64 82 | ) 83 | query := r.DB(ctx) 84 | if request.Email != "" { 85 | query = query.Where("email = ?", request.Email) 86 | } 87 | if request.Nickname != "" { 88 | query = query.Where("nickname = ?", request.Nickname) 89 | } 90 | if request.RoleId != 0 { 91 | query = query.Where("role_id = ?", request.RoleId) 92 | } 93 | if request.DeptId != 0 { 94 | query = query.Where("dept_id = ?", request.DeptId) 95 | } 96 | if err := query.Model(Model{}).Count(&total).Error; err != nil { 97 | return 0, nil, err 98 | } 99 | if err := query.Order("id desc").Preload("Role").Preload("Department").Offset((request.Page - 1) * request.Size).Limit(request.Size).Find(&users).Error; err != nil { 100 | return total, nil, err 101 | } 102 | return total, &users, nil 103 | } 104 | 105 | func (r *userRepository) Delete(ctx context.Context, id int64) error { 106 | if err := r.DB(ctx).Delete(&Model{}, id).Error; err != nil { 107 | return err 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /internal/user/service.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "golang.org/x/crypto/bcrypt" 8 | "helloadmin/internal/api" 9 | "helloadmin/internal/ecode" 10 | "helloadmin/pkg/helper/generate" 11 | "helloadmin/pkg/helper/sid" 12 | "helloadmin/pkg/jwt" 13 | ) 14 | 15 | type Service interface { 16 | Register(ctx context.Context, req *RegisterRequest) error 17 | Login(ctx context.Context, req *LoginRequest) (*LoginResponse, error) 18 | GetProfileByUserId(ctx context.Context, userId string) (*ProfileData, error) 19 | GetProfileById(ctx context.Context, id int64) (*ProfileData, error) 20 | Update(ctx context.Context, id int64, req *UpdateRequest) error 21 | Search(ctx context.Context, request *FindRequest) (*Response, error) 22 | Delete(ctx context.Context, id int64) error 23 | } 24 | 25 | func NewService(sid *sid.Sid, jwt *jwt.JWT, repo Repository) Service { 26 | return &userService{ 27 | sid: sid, 28 | jwt: jwt, 29 | repo: repo, 30 | } 31 | } 32 | 33 | type userService struct { 34 | sid *sid.Sid 35 | jwt *jwt.JWT 36 | repo Repository 37 | } 38 | 39 | func (s *userService) Register(ctx context.Context, req *RegisterRequest) error { 40 | // check username 41 | if user, err := s.repo.GetByEmail(ctx, req.Email); err == nil && user != nil { 42 | return ecode.ErrEmailAlreadyUse 43 | } 44 | 45 | salt := generate.RandomString(16) 46 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password+salt), bcrypt.DefaultCost) 47 | if err != nil { 48 | return err 49 | } 50 | // Generate user ID 51 | userId, err := s.sid.GenString() 52 | if err != nil { 53 | return err 54 | } 55 | 56 | user := &Model{ 57 | UserId: userId, 58 | Email: req.Email, 59 | Password: string(hashedPassword), 60 | Salt: salt, 61 | Nickname: req.Nickname, 62 | RoleId: req.RoleId, 63 | DeptId: req.DeptId, 64 | CreatedAt: time.Now(), 65 | UpdatedAt: time.Now(), 66 | } 67 | // Transaction 68 | if err = s.repo.Create(ctx, user); err != nil { 69 | return err 70 | } 71 | return nil 72 | } 73 | 74 | func (s *userService) Search(ctx context.Context, req *FindRequest) (*Response, error) { 75 | var response Response 76 | total, items, err := s.repo.Search(ctx, req) 77 | if err != nil { 78 | return nil, err 79 | } 80 | response.Items = make([]ProfileData, 0) 81 | if total > 0 { 82 | for _, item := range *items { 83 | response.Items = append(response.Items, ProfileData{ 84 | Id: item.ID, 85 | UserId: item.UserId, 86 | Nickname: item.Nickname, 87 | Email: item.Email, 88 | Department: struct { 89 | Id uint `json:"id"` 90 | Name string `json:"name"` 91 | }{ 92 | Id: item.Department.ID, 93 | Name: item.Department.Name, 94 | }, 95 | Role: struct { 96 | Id uint `json:"id"` 97 | Name string `json:"name"` 98 | }{ 99 | Id: item.Role.ID, 100 | Name: item.Role.Name, 101 | }, 102 | RoleId: item.RoleId, 103 | DeptId: item.DeptId, 104 | CreatedAt: item.CreatedAt.Format(time.DateTime), 105 | UpdatedAt: item.UpdatedAt.Format(time.DateTime), 106 | }) 107 | } 108 | } 109 | response.Pagination = api.Pagination{ 110 | Page: req.Page, 111 | Size: req.Size, 112 | Count: int(total), 113 | } 114 | return &response, nil 115 | } 116 | 117 | func (s *userService) Login(ctx context.Context, req *LoginRequest) (*LoginResponse, error) { 118 | user, err := s.repo.GetByEmail(ctx, req.Email) 119 | if err != nil || user == nil { 120 | return nil, ecode.ErrUserNotFound 121 | } 122 | if err = bcrypt.CompareHashAndPassword([]byte(user.Password+user.Salt), []byte(req.Password+user.Salt)); err != nil { 123 | return nil, ecode.ErrPasswordIncorrect 124 | } 125 | expiresAt := time.Now().Add(time.Hour * 24 * 90) 126 | token, err := s.jwt.GenToken(user.UserId, expiresAt) 127 | if err != nil { 128 | return nil, err 129 | } 130 | return &LoginResponse{ 131 | AccessToken: token, 132 | ExpiresAt: expiresAt.Format(time.DateTime), 133 | TokenType: "Bearer", 134 | }, nil 135 | } 136 | 137 | func (s *userService) GetProfileByUserId(ctx context.Context, userId string) (*ProfileData, error) { 138 | user, err := s.repo.GetByUserId(ctx, userId) 139 | if err != nil { 140 | return nil, err 141 | } 142 | return profile(user), nil 143 | } 144 | 145 | func (s *userService) GetProfileById(ctx context.Context, id int64) (*ProfileData, error) { 146 | user, err := s.repo.GetById(ctx, id) 147 | if err != nil { 148 | return nil, err 149 | } 150 | return profile(user), nil 151 | } 152 | 153 | func (s *userService) Update(ctx context.Context, id int64, req *UpdateRequest) error { 154 | user, err := s.repo.GetById(ctx, id) 155 | if err != nil { 156 | return err 157 | } 158 | if user.RoleId == 0 && req.RoleId != 0 { 159 | return ecode.ErrAdminUserCanNotModify 160 | } 161 | user.Email = req.Email 162 | user.Nickname = req.Nickname 163 | user.RoleId = uint(req.RoleId) 164 | user.DeptId = uint(req.DeptId) 165 | user.UpdatedAt = time.Now() 166 | if err = s.repo.Update(ctx, user); err != nil { 167 | return err 168 | } 169 | return nil 170 | } 171 | 172 | func (s *userService) Delete(ctx context.Context, id int64) error { 173 | return s.repo.Delete(ctx, id) 174 | } 175 | 176 | func profile(m *Model) *ProfileData { 177 | if m == nil { 178 | return nil 179 | } 180 | return &ProfileData{ 181 | Id: m.ID, 182 | UserId: m.UserId, 183 | Nickname: m.Nickname, 184 | Email: m.Email, 185 | RoleId: m.RoleId, 186 | DeptId: m.DeptId, 187 | Department: struct { 188 | Id uint `json:"id"` 189 | Name string `json:"name"` 190 | }{ 191 | Id: m.Department.ID, 192 | Name: m.Department.Name, 193 | }, 194 | Role: struct { 195 | Id uint `json:"id"` 196 | Name string `json:"name"` 197 | }{ 198 | Id: m.Role.ID, 199 | Name: m.Role.Name, 200 | }, 201 | CreatedAt: m.CreatedAt.Format(time.DateTime), 202 | UpdatedAt: m.UpdatedAt.Format(time.DateTime), 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /pkg/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "helloadmin/pkg/server" 11 | ) 12 | 13 | type App struct { 14 | name string 15 | servers []server.Server 16 | } 17 | 18 | type Option func(a *App) 19 | 20 | func NewApp(opts ...Option) *App { 21 | a := &App{} 22 | for _, opt := range opts { 23 | opt(a) 24 | } 25 | return a 26 | } 27 | 28 | func WithServer(servers ...server.Server) Option { 29 | return func(a *App) { 30 | a.servers = servers 31 | } 32 | } 33 | 34 | func WithName(name string) Option { 35 | return func(a *App) { 36 | a.name = name 37 | } 38 | } 39 | 40 | func (a *App) Run(ctx context.Context) error { 41 | var cancel context.CancelFunc 42 | ctx, cancel = context.WithCancel(ctx) 43 | defer cancel() 44 | 45 | signals := make(chan os.Signal, 1) 46 | signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) 47 | 48 | for _, srv := range a.servers { 49 | go func(srv server.Server) { 50 | err := srv.Start(ctx) 51 | if err != nil { 52 | log.Printf("Server start err: %v", err) 53 | } 54 | }(srv) 55 | } 56 | 57 | select { 58 | case <-signals: 59 | // Received termination signal 60 | log.Println("Received termination signal") 61 | case <-ctx.Done(): 62 | // Context canceled 63 | log.Println("Context canceled") 64 | } 65 | 66 | // Gracefully stop the servers 67 | for _, srv := range a.servers { 68 | err := srv.Stop(ctx) 69 | if err != nil { 70 | log.Printf("Server stop err: %v", err) 71 | } 72 | } 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func NewConfig(p string) *viper.Viper { 11 | envConf := os.Getenv("APP_CONF") 12 | if envConf == "" { 13 | envConf = p 14 | } 15 | fmt.Println("load conf file:", envConf) 16 | return getConfig(envConf) 17 | } 18 | 19 | func getConfig(path string) *viper.Viper { 20 | conf := viper.New() 21 | conf.SetConfigFile(path) 22 | err := conf.ReadInConfig() 23 | if err != nil { 24 | panic(err) 25 | } 26 | return conf 27 | } 28 | -------------------------------------------------------------------------------- /pkg/helper/convert/convert.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | const ( 4 | base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 5 | ) 6 | 7 | func IntToBase62(n int) string { 8 | if n == 0 { 9 | return string(base62[0]) 10 | } 11 | 12 | var result []byte 13 | for n > 0 { 14 | result = append(result, base62[n%62]) 15 | n /= 62 16 | } 17 | 18 | // 反转字符串 19 | for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 { 20 | result[i], result[j] = result[j], result[i] 21 | } 22 | 23 | return string(result) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/helper/generate/generate.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | func RandomString(length int) string { 9 | source := rand.NewSource(time.Now().UnixNano()) 10 | random := rand.New(source) 11 | 12 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 13 | result := make([]byte, length) 14 | 15 | for i := range result { 16 | result[i] = charset[random.Intn(len(charset))] 17 | } 18 | 19 | return string(result) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/helper/md5/md5.go: -------------------------------------------------------------------------------- 1 | package md5 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | ) 7 | 8 | func Md5(str string) string { 9 | hash := md5.Sum([]byte(str)) 10 | return hex.EncodeToString(hash[:]) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/helper/sid/sid.go: -------------------------------------------------------------------------------- 1 | package sid 2 | 3 | import ( 4 | "github.com/sony/sonyflake" 5 | "helloadmin/pkg/helper/convert" 6 | ) 7 | 8 | type Sid struct { 9 | sf *sonyflake.Sonyflake 10 | } 11 | 12 | func NewSid() *Sid { 13 | sf := sonyflake.NewSonyflake(sonyflake.Settings{}) 14 | if sf == nil { 15 | panic("sonyflake not created") 16 | } 17 | return &Sid{sf} 18 | } 19 | 20 | func (s Sid) GenString() (string, error) { 21 | id, err := s.sf.NextID() 22 | if err != nil { 23 | return "", err 24 | } 25 | return convert.IntToBase62(int(id)), nil 26 | } 27 | 28 | func (s Sid) GenUint64() (uint64, error) { 29 | return s.sf.NextID() 30 | } 31 | -------------------------------------------------------------------------------- /pkg/helper/uuid/uuid.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import "github.com/google/uuid" 4 | 5 | func GenUUID() string { 6 | return uuid.NewString() 7 | } 8 | -------------------------------------------------------------------------------- /pkg/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "time" 7 | 8 | "github.com/golang-jwt/jwt/v5" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | type JWT struct { 13 | key []byte 14 | } 15 | 16 | type MyCustomClaims struct { 17 | UserId string 18 | jwt.RegisteredClaims 19 | } 20 | 21 | func NewJwt(conf *viper.Viper) *JWT { 22 | return &JWT{key: []byte(conf.GetString("security.jwt.key"))} 23 | } 24 | 25 | func (j *JWT) GenToken(userId string, expiresAt time.Time) (string, error) { 26 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, MyCustomClaims{ 27 | UserId: userId, 28 | RegisteredClaims: jwt.RegisteredClaims{ 29 | ExpiresAt: jwt.NewNumericDate(expiresAt), 30 | IssuedAt: jwt.NewNumericDate(time.Now()), 31 | NotBefore: jwt.NewNumericDate(time.Now()), 32 | Issuer: "", 33 | Subject: "", 34 | ID: "", 35 | Audience: []string{}, 36 | }, 37 | }) 38 | 39 | // Sign and get the complete encoded token as a string using the key 40 | tokenString, err := token.SignedString(j.key) 41 | if err != nil { 42 | return "", err 43 | } 44 | return tokenString, nil 45 | } 46 | 47 | func (j *JWT) ParseToken(tokenString string) (*MyCustomClaims, error) { 48 | re := regexp.MustCompile(`(?i)Bearer `) 49 | tokenString = re.ReplaceAllString(tokenString, "") 50 | if tokenString == "" { 51 | return nil, errors.New("token is empty") 52 | } 53 | token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) { 54 | return j.key, nil 55 | }) 56 | if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid { 57 | return claims, nil 58 | } else { 59 | return nil, err 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/spf13/viper" 10 | "go.uber.org/zap" 11 | "go.uber.org/zap/zapcore" 12 | "gopkg.in/natefinch/lumberjack.v2" 13 | ) 14 | 15 | const ctxLoggerKey = "zapLogger" 16 | 17 | type Logger struct { 18 | *zap.Logger 19 | } 20 | 21 | func NewLog(conf *viper.Viper) *Logger { 22 | // log address "out.log" User-defined 23 | lp := conf.GetString("log.log_file_name") 24 | lv := conf.GetString("log.log_level") 25 | var level zapcore.Level 26 | // debug 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------