├── server ├── initialize │ ├── other.go │ ├── gorm_mysql.go │ └── router.go ├── router │ ├── dashboard.go │ └── system.go ├── model │ ├── system │ │ ├── sys_authority_menu.go │ │ ├── sys_authority.go │ │ ├── sys_user.go │ │ ├── sys_base_menu.go │ │ └── sys_operation_log.go │ └── common │ │ └── response │ │ └── response.go ├── global │ └── global.go ├── utils │ ├── common.go │ ├── bcrypt.go │ └── jwt.go ├── main.go ├── core │ ├── server.go │ └── viper.go ├── config │ ├── config.yaml │ └── config.go ├── api │ └── v1 │ │ ├── dashboard.go │ │ ├── base.go │ │ ├── upload.go │ │ ├── sys_menu.go │ │ └── sys_operation_log.go ├── middleware │ ├── jwt.go │ └── operation_log.go ├── go.mod ├── cmd │ └── admin_tool.go └── service │ └── system │ └── sys_operation_log.go ├── web ├── src │ ├── utils │ │ ├── auth.js │ │ ├── request.js │ │ ├── route.js │ │ └── permission.js │ ├── api │ │ ├── dashboard.js │ │ ├── upload.js │ │ ├── system │ │ │ ├── permission.js │ │ │ ├── menu.js │ │ │ ├── role.js │ │ │ ├── operation-log.js │ │ │ └── user.js │ │ └── user.js │ ├── main.js │ ├── layout │ │ ├── index.vue │ │ └── components │ │ │ ├── Navbar.vue │ │ │ └── Sidebar.vue │ ├── views │ │ ├── error │ │ │ └── 404.vue │ │ ├── test-table-width.vue │ │ ├── PermissionTest.vue │ │ ├── system │ │ │ ├── role │ │ │ │ └── permission.vue │ │ │ ├── user │ │ │ │ └── index.vue │ │ │ └── menu │ │ │ │ └── index.vue │ │ ├── dashboard │ │ │ └── index.vue │ │ ├── PermissionDemo.vue │ │ ├── login │ │ │ └── index.vue │ │ └── profile │ │ │ └── index.vue │ ├── App.vue │ ├── style │ │ └── index.scss │ ├── stores │ │ └── user.js │ ├── router │ │ └── index.js │ └── components │ │ └── IconSelector.vue ├── public │ ├── robots.txt │ └── version.json ├── vite.config.js ├── package.json └── index.html └── README.md /server/initialize/other.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | func OtherInit() { 4 | // 其他初始化操作 5 | } 6 | 7 | func Timer() { 8 | // 定时器初始化 9 | } 10 | 11 | func DBList() { 12 | // 数据库列表初始化 13 | } 14 | 15 | func Redis() { 16 | // Redis 初始化 17 | } 18 | -------------------------------------------------------------------------------- /web/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'Admin-Token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } -------------------------------------------------------------------------------- /web/src/api/dashboard.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取仪表板统计数据 4 | export function getDashboardStats() { 5 | return request({ 6 | url: '/dashboard/stats', 7 | method: 'get' 8 | }) 9 | } 10 | 11 | // 获取系统信息 12 | export function getSystemInfo() { 13 | return request({ 14 | url: '/dashboard/systemInfo', 15 | method: 'get' 16 | }) 17 | } -------------------------------------------------------------------------------- /web/src/api/upload.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 上传头像 4 | export function uploadAvatar(file) { 5 | const formData = new FormData() 6 | formData.append('file', file) 7 | 8 | return request({ 9 | url: '/upload/avatar', 10 | method: 'post', 11 | data: formData, 12 | headers: { 13 | 'Content-Type': 'multipart/form-data' 14 | } 15 | }) 16 | } -------------------------------------------------------------------------------- /server/router/dashboard.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | v1 "server/api/v1" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func InitDashboardRouter(Router *gin.RouterGroup) { 10 | dashboardRouter := Router.Group("dashboard") 11 | { 12 | dashboardRouter.GET("stats", v1.GetDashboardStats) // 获取仪表板统计数据 13 | dashboardRouter.GET("systemInfo", v1.GetSystemInfo) // 获取系统信息 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/model/system/sys_authority_menu.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | // SysAuthorityMenu 角色菜单关联表 8 | type SysAuthorityMenu struct { 9 | gorm.Model 10 | AuthorityId uint `json:"authorityId" gorm:"comment:角色ID"` 11 | BaseMenuId uint `json:"baseMenuId" gorm:"comment:菜单ID"` 12 | } 13 | 14 | func (SysAuthorityMenu) TableName() string { 15 | return "sys_authority_menus" 16 | } 17 | -------------------------------------------------------------------------------- /server/global/global.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/casbin/casbin/v2" 7 | "github.com/go-redis/redis/v8" 8 | "github.com/spf13/viper" 9 | "gorm.io/gorm" 10 | 11 | "server/config" 12 | ) 13 | 14 | var ( 15 | DB *gorm.DB 16 | REDIS *redis.Client 17 | CONFIG config.Server 18 | VP *viper.Viper 19 | CASBIN *casbin.Enforcer 20 | TIMER sync.Map 21 | lock sync.RWMutex 22 | ) 23 | -------------------------------------------------------------------------------- /server/utils/common.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | ) 7 | 8 | // RandomString 生成随机字符串 9 | func RandomString(length int) string { 10 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 11 | b := make([]byte, length) 12 | for i := range b { 13 | n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) 14 | b[i] = charset[n.Int64()] 15 | } 16 | return string(b) 17 | } 18 | -------------------------------------------------------------------------------- /web/src/api/system/permission.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取角色的菜单权限 4 | export function getRoleMenus(roleId) { 5 | return request({ 6 | url: `/system/role/${roleId}/menus`, 7 | method: 'get' 8 | }) 9 | } 10 | 11 | // 给角色分配菜单权限 12 | export function assignRoleMenus(roleId, menuIds) { 13 | return request({ 14 | url: `/system/role/${roleId}/menus`, 15 | method: 'post', 16 | data: { 17 | menuIds 18 | } 19 | }) 20 | } -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # Go-Gin-Element-Admin System 2 | # Version: 1.0.0 3 | # Framework: Vue3 + Element-Plus + Go + Gin 4 | # Build: 2024-12-10 5 | 6 | User-agent: * 7 | Allow: / 8 | 9 | # Admin System Paths 10 | Disallow: /api/ 11 | Disallow: /admin/ 12 | Disallow: /system/ 13 | Disallow: /login/ 14 | Disallow: /uploads/ 15 | 16 | # System Information 17 | # Powered by Go-Gin-Element-Admin 18 | # Author: taiguangyin 19 | # Technology Stack: Vue3, Element-Plus, Go, Gin, MySQL, Redis 20 | # Admin Dashboard System -------------------------------------------------------------------------------- /server/utils/bcrypt.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | ) 7 | 8 | // 固定的密码盐 9 | const PASSWORD_SALT = "go-gin-element-admin-2025" 10 | 11 | // BcryptHash 使用 MD5 + 固定盐对密码进行加密 12 | func BcryptHash(password string) string { 13 | // 用户密码 + 固定盐 14 | saltedPassword := password + PASSWORD_SALT 15 | // MD5加密并转为小写 16 | hash := md5.Sum([]byte(saltedPassword)) 17 | return fmt.Sprintf("%x", hash) 18 | } 19 | 20 | // BcryptCheck 校验密码 21 | func BcryptCheck(password, hash string) bool { 22 | // 使用相同的方式加密输入的密码 23 | inputHash := BcryptHash(password) 24 | // 比较哈希值 25 | return inputHash == hash 26 | } 27 | -------------------------------------------------------------------------------- /web/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { resolve } from 'path' 4 | 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | resolve: { 8 | alias: { 9 | '@': resolve(__dirname, 'src') 10 | } 11 | }, 12 | server: { 13 | port: 8080, 14 | proxy: { 15 | '/api': { 16 | target: 'http://localhost:8888', 17 | changeOrigin: true, 18 | //rewrite: (path) => path.replace(/^\/api/, '') 19 | } 20 | } 21 | }, 22 | build: { 23 | outDir: 'dist', 24 | assetsDir: 'assets', 25 | minify: 'terser', 26 | terserOptions: { 27 | compress: { 28 | drop_console: true, 29 | drop_debugger: true 30 | } 31 | } 32 | } 33 | }) -------------------------------------------------------------------------------- /web/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import { createPinia } from 'pinia' 5 | import ElementPlus from 'element-plus' 6 | import 'element-plus/dist/index.css' 7 | import * as ElementPlusIconsVue from '@element-plus/icons-vue' 8 | import { permissionDirective, roleDirective } from '@/utils/permission' 9 | import '@/style/index.scss' 10 | 11 | const app = createApp(App) 12 | 13 | // 注册所有图标 14 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 15 | app.component(key, component) 16 | } 17 | 18 | app.use(createPinia()) 19 | app.use(router) 20 | app.use(ElementPlus) 21 | 22 | // 注册权限指令 23 | app.directive('permission', permissionDirective) 24 | app.directive('role', roleDirective) 25 | 26 | app.mount('#app') -------------------------------------------------------------------------------- /web/public/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "system": "Go-Gin-Element-Admin", 3 | "version": "1.0.0", 4 | "author": "taiguangyin", 5 | "description": "基于Go+Gin+Vue3+Element-Plus构建的后台管理系统", 6 | "buildTime": "2024-12-10", 7 | "framework": { 8 | "frontend": "Vue3 + Element-Plus + Vite", 9 | "backend": "Go + Gin + GORM", 10 | "database": "MySQL 8.0", 11 | "cache": "Redis" 12 | }, 13 | "technology": [ 14 | "Vue3", 15 | "Element-Plus", 16 | "Vite", 17 | "Go", 18 | "Gin", 19 | "GORM", 20 | "MySQL", 21 | "Redis", 22 | "JWT", 23 | "RBAC" 24 | ], 25 | "features": [ 26 | "用户管理", 27 | "角色管理", 28 | "菜单管理", 29 | "权限控制", 30 | "操作日志", 31 | "个人中心", 32 | "仪表板" 33 | ], 34 | "github": "https://github.com/taiguangyin/go-gin-element-admin", 35 | "license": "MIT", 36 | "fingerprint": "go-gin-element-admin-v1.0.0-taiguangyin" 37 | } -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "go-gin-element-admin-web", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "serve": "vite preview" 11 | }, 12 | "dependencies": { 13 | "vue": "^3.4.21", 14 | "vue-router": "^4.3.0", 15 | "pinia": "^2.1.7", 16 | "element-plus": "^2.6.3", 17 | "@element-plus/icons-vue": "^2.3.1", 18 | "axios": "^1.6.8", 19 | "js-cookie": "^3.0.5", 20 | "nprogress": "^0.2.0", 21 | "mitt": "^3.0.1", 22 | "path-to-regexp": "^6.2.2", 23 | "echarts": "^5.5.0", 24 | "dayjs": "^1.11.10" 25 | }, 26 | "devDependencies": { 27 | "@vitejs/plugin-vue": "^5.0.4", 28 | "vite": "^5.2.8", 29 | "sass": "^1.72.0", 30 | "@types/js-cookie": "^3.0.6", 31 | "@types/nprogress": "^0.2.3" 32 | } 33 | } -------------------------------------------------------------------------------- /server/model/system/sys_authority.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type SysAuthority struct { 10 | CreatedAt time.Time `json:"createdAt" gorm:"comment:创建时间"` 11 | UpdatedAt time.Time `json:"updatedAt" gorm:"comment:更新时间"` 12 | DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:删除时间"` 13 | AuthorityId uint `json:"authorityId" gorm:"not null;unique;primaryKey;autoIncrement;comment:角色ID;size:90"` 14 | AuthorityName string `json:"authorityName" gorm:"comment:角色名"` 15 | AuthorityCode string `json:"authorityCode" gorm:"column:authority_code;comment:角色编码;unique"` 16 | ParentId *uint `json:"parentId" gorm:"comment:父角色ID"` 17 | DefaultRouter string `json:"defaultRouter" gorm:"comment:默认菜单;default:dashboard"` 18 | } 19 | 20 | func (SysAuthority) TableName() string { 21 | return "sys_authorities" 22 | } 23 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "server/core" 6 | "server/global" 7 | "server/initialize" 8 | ) 9 | 10 | // @title Go-Gin-Element-Admin API 11 | // @version 1.0 12 | // @description 基于 Gin + Vue3 + Element-Admin 的后台管理系统 13 | // @securityDefinitions.apikey Bearer 14 | // @in header 15 | // @name Authorization 16 | // @BasePath / 17 | func main() { 18 | global.VP = core.Viper() // 初始化Viper 19 | initialize.OtherInit() 20 | global.DB = initialize.Gorm() // gorm连接数据库 21 | initialize.Timer() 22 | initialize.DBList() 23 | if global.DB != nil { 24 | initialize.RegisterTables() // 初始化表 25 | // 程序结束前关闭数据库链接 26 | db, _ := global.DB.DB() 27 | defer db.Close() 28 | } 29 | core.RunWindowsServer() 30 | } 31 | 32 | func init() { 33 | fmt.Println(` 34 | 欢迎使用 go-gin-element-admin 35 | 当前版本:v1.0.0 36 | 默认自动化文档地址:http://127.0.0.1:8888/swagger/index.html 37 | 默认前端文件运行地址:http://127.0.0.1:8080 38 | `) 39 | } 40 | -------------------------------------------------------------------------------- /server/initialize/gorm_mysql.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gorm.io/driver/mysql" 7 | "gorm.io/gorm" 8 | 9 | "server/global" 10 | ) 11 | 12 | // GormMysql 初始化Mysql数据库 13 | func GormMysql() *gorm.DB { 14 | m := global.CONFIG.Mysql 15 | if m.DbName == "" { 16 | return nil 17 | } 18 | dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", 19 | m.Username, 20 | m.Password, 21 | m.Path, 22 | m.DbName, 23 | m.Config, 24 | ) 25 | mysqlConfig := mysql.Config{ 26 | DSN: dsn, // DSN data source name 27 | DefaultStringSize: 191, // string 类型字段的默认长度 28 | SkipInitializeWithVersion: false, // 根据版本自动配置 29 | } 30 | if db, err := gorm.Open(mysql.New(mysqlConfig), &gorm.Config{}); err != nil { 31 | return nil 32 | } else { 33 | db.InstanceSet("gorm:table_options", "ENGINE=InnoDB") 34 | sqlDB, _ := db.DB() 35 | sqlDB.SetMaxIdleConns(m.MaxIdleConns) 36 | sqlDB.SetMaxOpenConns(m.MaxOpenConns) 37 | return db 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /web/src/api/system/menu.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取菜单列表 4 | export function getMenuList(params) { 5 | return request({ 6 | url: '/system/menu/list', 7 | method: 'get', 8 | params 9 | }) 10 | } 11 | 12 | // 获取菜单树结构 13 | export function getMenuTree() { 14 | return request({ 15 | url: '/system/menu/tree', 16 | method: 'get' 17 | }) 18 | } 19 | 20 | // 根据ID获取菜单 21 | export function getMenuById(id) { 22 | return request({ 23 | url: `/system/menu/${id}`, 24 | method: 'get' 25 | }) 26 | } 27 | 28 | // 创建菜单 29 | export function createMenu(data) { 30 | return request({ 31 | url: '/system/menu', 32 | method: 'post', 33 | data 34 | }) 35 | } 36 | 37 | // 更新菜单 38 | export function updateMenu(id, data) { 39 | return request({ 40 | url: `/system/menu/${id}`, 41 | method: 'put', 42 | data 43 | }) 44 | } 45 | 46 | // 删除菜单 47 | export function deleteMenu(id) { 48 | return request({ 49 | url: `/system/menu/${id}`, 50 | method: 'delete' 51 | }) 52 | } -------------------------------------------------------------------------------- /web/src/api/system/role.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取角色列表 4 | export function getRoleList(params) { 5 | return request({ 6 | url: '/system/role/list', 7 | method: 'get', 8 | params 9 | }) 10 | } 11 | 12 | // 获取所有角色(下拉选项用) 13 | export function getAllRoles() { 14 | return request({ 15 | url: '/system/role/all', 16 | method: 'get' 17 | }) 18 | } 19 | 20 | // 根据ID获取角色 21 | export function getRoleById(id) { 22 | return request({ 23 | url: `/system/role/${id}`, 24 | method: 'get' 25 | }) 26 | } 27 | 28 | // 创建角色 29 | export function createRole(data) { 30 | return request({ 31 | url: '/system/role', 32 | method: 'post', 33 | data 34 | }) 35 | } 36 | 37 | // 更新角色 38 | export function updateRole(id, data) { 39 | return request({ 40 | url: `/system/role/${id}`, 41 | method: 'put', 42 | data 43 | }) 44 | } 45 | 46 | // 删除角色 47 | export function deleteRole(id) { 48 | return request({ 49 | url: `/system/role/${id}`, 50 | method: 'delete' 51 | }) 52 | } -------------------------------------------------------------------------------- /server/model/system/sys_user.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type SysUser struct { 9 | gorm.Model 10 | UUID uuid.UUID `json:"uuid" gorm:"index;comment:用户UUID"` 11 | Username string `json:"username" gorm:"index;comment:用户登录名"` 12 | Password string `json:"password" gorm:"comment:用户登录密码"` 13 | NickName string `json:"nickName" gorm:"default:系统用户;comment:用户昵称"` 14 | SideMode string `json:"sideMode" gorm:"default:dark;comment:用户侧边主题"` 15 | HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` 16 | BaseColor string `json:"baseColor" gorm:"default:#fff;comment:基础颜色"` 17 | ActiveColor string `json:"activeColor" gorm:"default:#1890ff;comment:活跃颜色"` 18 | AuthorityId uint `json:"authorityId" gorm:"default:888;index;comment:用户角色ID"` 19 | Phone string `json:"phone" gorm:"comment:用户手机号"` 20 | Email string `json:"email" gorm:"comment:用户邮箱"` 21 | Enable int `json:"enable" gorm:"default:1;comment:用户是否被冻结 1正常 2冻结"` 22 | } 23 | 24 | func (SysUser) TableName() string { 25 | return "sys_users" 26 | } 27 | -------------------------------------------------------------------------------- /web/src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 登录 4 | export function login(data) { 5 | return request({ 6 | url: '/base/login', 7 | method: 'post', 8 | data 9 | }) 10 | } 11 | 12 | // 获取用户信息 13 | export function getUserInfo() { 14 | return request({ 15 | url: '/user/getUserInfo', 16 | method: 'get' 17 | }) 18 | } 19 | 20 | // 获取用户列表 21 | export function getUserList(params) { 22 | return request({ 23 | url: '/user/getUserList', 24 | method: 'get', 25 | params 26 | }) 27 | } 28 | 29 | // 创建用户 30 | export function createUser(data) { 31 | return request({ 32 | url: '/user/register', 33 | method: 'post', 34 | data 35 | }) 36 | } 37 | 38 | // 更新用户 39 | export function updateUser(data) { 40 | return request({ 41 | url: '/user/setUserInfo', 42 | method: 'put', 43 | data 44 | }) 45 | } 46 | 47 | // 删除用户 48 | export function deleteUser(data) { 49 | return request({ 50 | url: '/user/deleteUser', 51 | method: 'delete', 52 | data 53 | }) 54 | } 55 | 56 | // 重置密码 57 | export function resetPassword(data) { 58 | return request({ 59 | url: '/user/resetPassword', 60 | method: 'post', 61 | data 62 | }) 63 | } -------------------------------------------------------------------------------- /server/core/server.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "server/global" 9 | "server/initialize" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | func initServer(address string, router *gin.Engine) *http.Server { 15 | return &http.Server{ 16 | Addr: address, 17 | Handler: router, 18 | ReadTimeout: 20 * time.Second, 19 | WriteTimeout: 20 * time.Second, 20 | MaxHeaderBytes: 1 << 20, 21 | } 22 | } 23 | 24 | func RunWindowsServer() { 25 | if global.CONFIG.System.UseMultipoint || global.CONFIG.System.UseRedis { 26 | // 初始化redis服务 27 | initialize.Redis() 28 | } 29 | 30 | Router := initialize.Routers() 31 | Router.Static("/form-generator", "./resource/page") 32 | 33 | address := fmt.Sprintf(":%d", global.CONFIG.System.Addr) 34 | s := initServer(address, Router) 35 | // 保证文本顺序输出 36 | // In order to ensure that the text order output can be deleted 37 | time.Sleep(10 * time.Microsecond) 38 | fmt.Printf(` 39 | 欢迎使用 go-gin-element-admin 40 | 当前版本:v1.0.0 41 | 默认自动化文档地址:http://127.0.0.1%s/swagger/index.html 42 | 默认前端文件运行地址:http://127.0.0.1:8080 43 | `, address) 44 | fmt.Printf(` 45 | 项目运行中... 46 | `) 47 | fmt.Println(s.ListenAndServe().Error()) 48 | } 49 | -------------------------------------------------------------------------------- /server/config/config.yaml: -------------------------------------------------------------------------------- 1 | # Go-Gin-Element-Admin 系统配置文件 2 | 3 | # JWT认证配置 4 | jwt: 5 | signing-key: 'go-gin-element-admin' 6 | expires-time: 604800 # 7天,单位秒 7 | buffer-time: 86400 # 1天,单位秒 8 | issuer: 'go-gin-element-admin' 9 | 10 | # 日志配置 11 | zap: 12 | level: 'info' 13 | format: 'console' 14 | prefix: '[GO-GIN-ELEMENT-ADMIN]' 15 | director: 'log' 16 | show-line: true 17 | encode-level: 'LowercaseColorLevelEncoder' 18 | stacktrace-key: 'stacktrace' 19 | log-in-console: true 20 | 21 | # Redis配置 22 | redis: 23 | db: 0 24 | addr: '127.0.0.1:6379' 25 | password: '' 26 | 27 | # 权限管理配置 28 | casbin: 29 | model-path: './resource/rbac_model.conf' 30 | 31 | # 系统基础配置 32 | system: 33 | env: 'public' # 环境模式: public-生产, develop-开发 34 | addr: 8888 # 服务端口 35 | db-type: 'mysql' # 数据库类型 36 | use-multipoint: false # 多点登录拦截 37 | use-redis: false # 使用Redis 38 | 39 | # MySQL数据库配置 40 | mysql: 41 | path: '127.0.0.1:3306' 42 | port: '3306' 43 | config: 'charset=utf8mb4&parseTime=True&loc=Local' 44 | db-name: 'go_gin_element_admin' 45 | username: 'root' 46 | password: '123456' 47 | max-idle-conns: 10 48 | max-open-conns: 100 49 | log-mode: '' 50 | log-zap: false 51 | 52 | # 跨域配置 53 | cors: 54 | mode: allow-all # 允许所有跨域请求,简化开发 -------------------------------------------------------------------------------- /server/model/common/response/response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type Response struct { 10 | Code int `json:"code"` 11 | Data interface{} `json:"data"` 12 | Msg string `json:"msg"` 13 | } 14 | 15 | const ( 16 | ERROR = 7 17 | SUCCESS = 0 18 | ) 19 | 20 | func Result(code int, data interface{}, msg string, c *gin.Context) { 21 | // 开始时间 22 | c.JSON(http.StatusOK, Response{ 23 | code, 24 | data, 25 | msg, 26 | }) 27 | } 28 | 29 | func Ok(c *gin.Context) { 30 | Result(SUCCESS, map[string]interface{}{}, "操作成功", c) 31 | } 32 | 33 | func OkWithMessage(message string, c *gin.Context) { 34 | Result(SUCCESS, map[string]interface{}{}, message, c) 35 | } 36 | 37 | func OkWithData(data interface{}, c *gin.Context) { 38 | Result(SUCCESS, data, "查询成功", c) 39 | } 40 | 41 | func OkWithDetailed(data interface{}, message string, c *gin.Context) { 42 | Result(SUCCESS, data, message, c) 43 | } 44 | 45 | func Fail(c *gin.Context) { 46 | Result(ERROR, map[string]interface{}{}, "操作失败", c) 47 | } 48 | 49 | func FailWithMessage(message string, c *gin.Context) { 50 | Result(ERROR, map[string]interface{}{}, message, c) 51 | } 52 | 53 | func FailWithDetailed(data interface{}, message string, c *gin.Context) { 54 | Result(ERROR, data, message, c) 55 | } 56 | -------------------------------------------------------------------------------- /server/core/viper.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "server/global" 9 | 10 | "github.com/fsnotify/fsnotify" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | // Viper 优先级 命令行 > 环境变量 > 默认值 15 | func Viper(path ...string) *viper.Viper { 16 | var config string 17 | 18 | if len(path) == 0 { 19 | flag.StringVar(&config, "c", "", "choose config file.") 20 | flag.Parse() 21 | if config == "" { // 判断命令行参数是否为空 22 | if configEnv := os.Getenv("ADMIN_CONFIG"); configEnv == "" { // 判断 ADMIN_CONFIG 环境变量是否为空 23 | config = "config/config.yaml" 24 | fmt.Printf("您正在使用config的默认值,config的路径为%v\n", config) 25 | } else { 26 | config = configEnv 27 | fmt.Printf("您正在使用ADMIN_CONFIG环境变量,config的路径为%v\n", config) 28 | } 29 | } else { 30 | fmt.Printf("您正在使用命令行的-c参数传递的值,config的路径为%v\n", config) 31 | } 32 | } else { 33 | config = path[0] 34 | fmt.Printf("您正在使用func Viper()传递的值,config的路径为%v\n", config) 35 | } 36 | 37 | v := viper.New() 38 | v.SetConfigFile(config) 39 | v.SetConfigType("yaml") 40 | err := v.ReadInConfig() 41 | if err != nil { 42 | panic(fmt.Errorf("Fatal error config file: %s \n", err)) 43 | } 44 | v.WatchConfig() 45 | 46 | v.OnConfigChange(func(e fsnotify.Event) { 47 | fmt.Println("config file changed:", e.Name) 48 | if err = v.Unmarshal(&global.CONFIG); err != nil { 49 | fmt.Println(err) 50 | } 51 | }) 52 | if err = v.Unmarshal(&global.CONFIG); err != nil { 53 | fmt.Println(err) 54 | } 55 | 56 | return v 57 | } 58 | -------------------------------------------------------------------------------- /web/src/api/system/operation-log.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取操作日志列表 4 | export function getOperationLogList(params) { 5 | return request({ 6 | url: '/system/operation-log/list', 7 | method: 'get', 8 | params 9 | }) 10 | } 11 | 12 | // 获取操作日志详情 13 | export function getOperationLogById(id) { 14 | return request({ 15 | url: `/system/operation-log/${id}`, 16 | method: 'get' 17 | }) 18 | } 19 | 20 | // 删除操作日志 21 | export function deleteOperationLog(id) { 22 | return request({ 23 | url: `/system/operation-log/${id}`, 24 | method: 'delete' 25 | }) 26 | } 27 | 28 | // 批量删除操作日志 29 | export function deleteOperationLogsByIds(ids) { 30 | return request({ 31 | url: '/system/operation-log/batch', 32 | method: 'delete', 33 | data: ids 34 | }) 35 | } 36 | 37 | // 清空操作日志 38 | export function clearOperationLogs() { 39 | return request({ 40 | url: '/system/operation-log/clear', 41 | method: 'delete' 42 | }) 43 | } 44 | 45 | // 清理指定天数前的操作日志 46 | export function clearOperationLogsByDays(days) { 47 | return request({ 48 | url: '/system/operation-log/clear-by-days', 49 | method: 'delete', 50 | data: { days } 51 | }) 52 | } 53 | 54 | // 获取操作统计信息 55 | export function getOperationStats() { 56 | return request({ 57 | url: '/system/operation-log/stats', 58 | method: 'get' 59 | }) 60 | } 61 | 62 | // 导出操作日志 63 | export function exportOperationLogs(params) { 64 | return request({ 65 | url: '/system/operation-log/export', 66 | method: 'get', 67 | params, 68 | responseType: 'blob' 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /web/src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /server/api/v1/dashboard.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "server/global" 5 | "server/model/system" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // DashboardStats 仪表板统计数据结构 12 | type DashboardStats struct { 13 | UserCount int64 `json:"userCount"` // 用户总数 14 | RoleCount int64 `json:"roleCount"` // 角色总数 15 | MenuCount int64 `json:"menuCount"` // 菜单总数 16 | OnlineCount int64 `json:"onlineCount"` // 在线用户数(暂时模拟) 17 | } 18 | 19 | // SystemInfo 系统信息结构 20 | type SystemInfo struct { 21 | SystemVersion string `json:"systemVersion"` 22 | GoVersion string `json:"goVersion"` 23 | GinVersion string `json:"ginVersion"` 24 | VueVersion string `json:"vueVersion"` 25 | ElementPlus string `json:"elementPlus"` 26 | } 27 | 28 | // GetDashboardStats 获取仪表板统计数据 29 | func GetDashboardStats(c *gin.Context) { 30 | var stats DashboardStats 31 | 32 | // 获取用户总数 33 | global.DB.Model(&system.SysUser{}).Count(&stats.UserCount) 34 | 35 | // 获取角色总数 36 | global.DB.Model(&system.SysAuthority{}).Count(&stats.RoleCount) 37 | 38 | // 获取菜单总数 39 | global.DB.Model(&system.SysBaseMenu{}).Count(&stats.MenuCount) 40 | 41 | // 在线用户数(这里暂时使用模拟数据,实际可以通过Redis或其他方式统计) 42 | stats.OnlineCount = 12 43 | 44 | c.JSON(http.StatusOK, gin.H{ 45 | "code": 0, 46 | "data": stats, 47 | "msg": "获取成功", 48 | }) 49 | } 50 | 51 | // GetSystemInfo 获取系统信息 52 | func GetSystemInfo(c *gin.Context) { 53 | info := SystemInfo{ 54 | SystemVersion: "v1.0.0", 55 | GoVersion: "1.21", 56 | GinVersion: "1.9.1", 57 | VueVersion: "3.4.21", 58 | ElementPlus: "2.6.3", 59 | } 60 | 61 | c.JSON(http.StatusOK, gin.H{ 62 | "code": 0, 63 | "data": info, 64 | "msg": "获取成功", 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /web/src/views/error/404.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | 35 | -------------------------------------------------------------------------------- /server/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "server/utils" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // JWTAuth JWT认证中间件 12 | func JWTAuth() gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | // 从header中获取token 15 | token := c.GetHeader("Authorization") 16 | if token == "" { 17 | c.JSON(http.StatusUnauthorized, gin.H{ 18 | "code": 401, 19 | "msg": "请求未携带token,无权限访问", 20 | }) 21 | c.Abort() 22 | return 23 | } 24 | 25 | // 移除Bearer前缀 26 | if strings.HasPrefix(token, "Bearer ") { 27 | token = token[7:] 28 | } 29 | 30 | // 解析token 31 | claims, err := utils.ParseToken(token) 32 | if err != nil { 33 | c.JSON(http.StatusUnauthorized, gin.H{ 34 | "code": 401, 35 | "msg": "无效的token", 36 | }) 37 | c.Abort() 38 | return 39 | } 40 | 41 | // 将用户信息存储到context中 42 | c.Set("userID", claims.UserID) 43 | c.Set("username", claims.Username) 44 | c.Set("authorityId", claims.AuthorityId) 45 | 46 | c.Next() 47 | } 48 | } 49 | 50 | // GetCurrentUser 从context中获取当前用户信息 51 | func GetCurrentUser(c *gin.Context) (userID uint, username string, authorityId uint, exists bool) { 52 | userIDInterface, exists1 := c.Get("userID") 53 | usernameInterface, exists2 := c.Get("username") 54 | authorityIdInterface, exists3 := c.Get("authorityId") 55 | 56 | if !exists1 || !exists2 || !exists3 { 57 | return 0, "", 0, false 58 | } 59 | 60 | userID, ok1 := userIDInterface.(uint) 61 | username, ok2 := usernameInterface.(string) 62 | authorityId, ok3 := authorityIdInterface.(uint) 63 | 64 | if !ok1 || !ok2 || !ok3 { 65 | return 0, "", 0, false 66 | } 67 | 68 | return userID, username, authorityId, true 69 | } 70 | -------------------------------------------------------------------------------- /server/utils/jwt.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/golang-jwt/jwt/v4" 8 | ) 9 | 10 | type Claims struct { 11 | UserID uint `json:"user_id"` 12 | Username string `json:"username"` 13 | AuthorityId uint `json:"authority_id"` 14 | jwt.RegisteredClaims 15 | } 16 | 17 | var jwtSecret = []byte("go-gin-element-admin-secret-key") 18 | 19 | // GenerateToken 生成JWT token 20 | func GenerateToken(userID uint, username string, authorityId uint) (string, error) { 21 | nowTime := time.Now() 22 | expireTime := nowTime.Add(7 * 24 * time.Hour) // 7天过期 23 | 24 | claims := Claims{ 25 | UserID: userID, 26 | Username: username, 27 | AuthorityId: authorityId, 28 | RegisteredClaims: jwt.RegisteredClaims{ 29 | ExpiresAt: jwt.NewNumericDate(expireTime), 30 | Issuer: "go-gin-element-admin", 31 | }, 32 | } 33 | 34 | tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 35 | token, err := tokenClaims.SignedString(jwtSecret) 36 | 37 | return token, err 38 | } 39 | 40 | // ParseToken 解析JWT token 41 | func ParseToken(token string) (*Claims, error) { 42 | tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) { 43 | return jwtSecret, nil 44 | }) 45 | 46 | if tokenClaims != nil { 47 | if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid { 48 | return claims, nil 49 | } 50 | } 51 | 52 | return nil, err 53 | } 54 | 55 | // ValidateToken 验证token是否有效 56 | func ValidateToken(token string) error { 57 | claims, err := ParseToken(token) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | if time.Now().After(claims.ExpiresAt.Time) { 63 | return errors.New("token已过期") 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /web/src/api/system/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取用户列表 4 | export function getUserList(params) { 5 | return request({ 6 | url: '/system/user/list', 7 | method: 'get', 8 | params 9 | }) 10 | } 11 | 12 | // 根据ID获取用户 13 | export function getUserById(id) { 14 | return request({ 15 | url: `/system/user/${id}`, 16 | method: 'get' 17 | }) 18 | } 19 | 20 | // 创建用户 21 | export function createUser(data) { 22 | return request({ 23 | url: '/system/user', 24 | method: 'post', 25 | data 26 | }) 27 | } 28 | 29 | // 更新用户 30 | export function updateUser(id, data) { 31 | return request({ 32 | url: `/system/user/${id}`, 33 | method: 'put', 34 | data 35 | }) 36 | } 37 | 38 | // 删除用户 39 | export function deleteUser(id) { 40 | return request({ 41 | url: `/system/user/${id}`, 42 | method: 'delete' 43 | }) 44 | } 45 | 46 | // 获取当前用户信息 47 | export function getUserInfo() { 48 | return request({ 49 | url: '/system/user/info', 50 | method: 'get' 51 | }) 52 | } 53 | 54 | // 更新当前用户信息 55 | export function updateUserInfo(data) { 56 | return request({ 57 | url: '/system/user/info', 58 | method: 'put', 59 | data 60 | }) 61 | } 62 | 63 | // 修改密码 64 | export function changePassword(data) { 65 | return request({ 66 | url: '/system/user/password', 67 | method: 'put', 68 | data 69 | }) 70 | } 71 | 72 | // 重置密码 73 | export function resetPassword(id) { 74 | return request({ 75 | url: `/system/user/${id}`, 76 | method: 'put', 77 | data: { 78 | password: 'RESET_PASSWORD_123456' 79 | } 80 | }) 81 | } 82 | 83 | // 获取用户菜单(根据权限动态返回) 84 | export function getUserMenus() { 85 | return request({ 86 | url: '/system/user/menus', 87 | method: 'get' 88 | }) 89 | } -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 46 | 47 | -------------------------------------------------------------------------------- /server/router/system.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | v1 "server/api/v1" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func InitSystemRouter(Router *gin.RouterGroup) { 10 | systemRouter := Router.Group("system") 11 | { 12 | // 用户管理路由 13 | userRouter := systemRouter.Group("user") 14 | { 15 | userRouter.GET("list", v1.GetUserList) // 获取用户列表 16 | userRouter.GET("info", v1.GetUserInfo) // 获取当前用户信息 17 | userRouter.GET("menus", v1.GetUserMenus) // 获取用户菜单 18 | userRouter.PUT("info", v1.UpdateUserInfo) // 更新当前用户信息 19 | userRouter.PUT("password", v1.ChangePassword) // 修改密码 20 | userRouter.GET(":id", v1.GetUserById) // 根据ID获取用户 21 | userRouter.POST("", v1.CreateUser) // 创建用户 22 | userRouter.PUT(":id", v1.UpdateUser) // 更新用户 23 | userRouter.DELETE(":id", v1.DeleteUser) // 删除用户 24 | } 25 | 26 | // 角色管理路由 27 | roleRouter := systemRouter.Group("role") 28 | { 29 | roleRouter.GET("list", v1.GetAuthorityList) // 获取角色列表 30 | roleRouter.GET("all", v1.GetAllAuthorities) // 获取所有角色(下拉选项) 31 | roleRouter.POST("", v1.CreateAuthority) // 创建角色 32 | roleRouter.GET(":id/menus", v1.GetAuthorityMenus) // 获取角色菜单权限 33 | roleRouter.POST(":id/menus", v1.AssignMenus) // 分配角色菜单权限 34 | roleRouter.GET(":id", v1.GetAuthorityById) // 根据ID获取角色 35 | roleRouter.PUT(":id", v1.UpdateAuthority) // 更新角色 36 | roleRouter.DELETE(":id", v1.DeleteAuthority) // 删除角色 37 | } 38 | 39 | // 菜单管理路由 40 | menuRouter := systemRouter.Group("menu") 41 | { 42 | menuRouter.GET("list", v1.GetMenuList) // 获取菜单列表 43 | menuRouter.GET("tree", v1.GetMenuTree) // 获取菜单树结构 44 | menuRouter.POST("", v1.CreateMenu) // 创建菜单 45 | menuRouter.GET(":id", v1.GetMenuById) // 根据ID获取菜单 46 | menuRouter.PUT(":id", v1.UpdateMenu) // 更新菜单 47 | menuRouter.DELETE(":id", v1.DeleteMenu) // 删除菜单 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /web/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { ElMessage } from 'element-plus' 3 | import { useUserStore } from '@/stores/user' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // 创建 axios 实例 7 | const service = axios.create({ 8 | baseURL: '/api', // api 的 base_url 9 | timeout: 5000 // 请求超时时间 10 | }) 11 | 12 | // request 拦截器 13 | service.interceptors.request.use( 14 | config => { 15 | // 在请求发送之前做一些处理 16 | const token = getToken() 17 | if (token) { 18 | // 让每个请求携带自定义 token 请根据实际情况自行修改 19 | config.headers['Authorization'] = 'Bearer ' + token 20 | } 21 | return config 22 | }, 23 | error => { 24 | // 处理请求错误 25 | console.log(error) // for debug 26 | return Promise.reject(error) 27 | } 28 | ) 29 | 30 | // response 拦截器 31 | service.interceptors.response.use( 32 | response => { 33 | const res = response.data 34 | 35 | // 如果自定义代码不是 0,则判断为错误 36 | if (res.code !== 0) { 37 | // 401: 未授权 38 | if (res.code === 401) { 39 | const userStore = useUserStore() 40 | userStore.logout() 41 | return Promise.reject(new Error('登录已过期,请重新登录')) 42 | } 43 | 44 | ElMessage({ 45 | message: res.msg || 'Error', 46 | type: 'error', 47 | duration: 5 * 1000 48 | }) 49 | 50 | return Promise.reject(new Error(res.msg || 'Error')) 51 | } else { 52 | return res 53 | } 54 | }, 55 | error => { 56 | console.log('err' + error) // for debug 57 | 58 | // 处理HTTP状态码401 59 | if (error.response && error.response.status === 401) { 60 | const userStore = useUserStore() 61 | userStore.logout() 62 | return Promise.reject(new Error('登录已过期,请重新登录')) 63 | } 64 | 65 | ElMessage({ 66 | message: error.message || '网络错误', 67 | type: 'error', 68 | duration: 5 * 1000 69 | }) 70 | return Promise.reject(error) 71 | } 72 | ) 73 | 74 | export default service -------------------------------------------------------------------------------- /server/model/system/sys_base_menu.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type SysBaseMenu struct { 8 | gorm.Model 9 | MenuLevel uint `json:"-"` 10 | ParentId *uint `json:"parentId" gorm:"comment:父菜单ID"` 11 | Path string `json:"path" gorm:"comment:路由path"` 12 | Name string `json:"name" gorm:"comment:路由name"` 13 | Hidden bool `json:"hidden" gorm:"comment:是否在列表隐藏"` 14 | Component string `json:"component" gorm:"comment:对应前端文件路径"` 15 | Sort int `json:"sort" gorm:"comment:排序标记"` 16 | ActiveName string `json:"activeName" gorm:"comment:高亮菜单"` 17 | KeepAlive bool `json:"keepAlive" gorm:"comment:是否缓存"` 18 | DefaultMenu bool `json:"defaultMenu" gorm:"comment:是否是基础路由(开发中)"` 19 | Title string `json:"title" gorm:"comment:菜单名"` 20 | Icon string `json:"icon" gorm:"comment:菜单图标"` 21 | CloseTab bool `json:"closeTab" gorm:"comment:自动关闭tab"` 22 | 23 | // 新增权限编码字段,用于按钮权限控制 24 | PermissionCode string `json:"permissionCode" gorm:"comment:权限编码,按钮类型必填"` 25 | MenuType string `json:"menuType" gorm:"comment:菜单类型 menu:菜单 button:按钮"` 26 | 27 | Children []SysBaseMenu `json:"children" gorm:"-"` 28 | Parameters []SysBaseMenuParameter `json:"parameters"` 29 | MenuBtn []SysBaseMenuBtn `json:"menuBtn"` 30 | } 31 | 32 | type SysBaseMenuParameter struct { 33 | gorm.Model 34 | SysBaseMenuID uint `json:"sysBaseMenuID" gorm:"comment:菜单ID"` 35 | Type string `json:"type" gorm:"comment:地址栏携带参数为params还是query"` 36 | Key string `json:"key" gorm:"comment:地址栏携带参数的key"` 37 | Value string `json:"value" gorm:"comment:地址栏携带参数的值"` 38 | } 39 | 40 | type SysBaseMenuBtn struct { 41 | gorm.Model 42 | Name string `json:"name" gorm:"comment:按钮关键key"` 43 | Desc string `json:"desc" gorm:"comment:按钮备注"` 44 | SysBaseMenuID uint `json:"sysBaseMenuID" gorm:"comment:菜单ID"` 45 | } 46 | 47 | func (SysBaseMenu) TableName() string { 48 | return "sys_base_menus" 49 | } 50 | -------------------------------------------------------------------------------- /server/model/system/sys_operation_log.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // SysOperationLog 操作日志表 10 | type SysOperationLog struct { 11 | gorm.Model 12 | UserID uint `json:"userId" gorm:"index;comment:用户ID"` 13 | Username string `json:"username" gorm:"comment:用户名"` 14 | Method string `json:"method" gorm:"comment:请求方法"` 15 | Path string `json:"path" gorm:"comment:请求路径"` 16 | OperationType string `json:"operationType" gorm:"comment:操作类型(CREATE/UPDATE/DELETE)"` 17 | Description string `json:"description" gorm:"comment:操作描述"` 18 | RequestBody string `json:"requestBody" gorm:"type:text;comment:请求参数"` 19 | ResponseBody string `json:"responseBody" gorm:"type:text;comment:响应结果"` 20 | IP string `json:"ip" gorm:"comment:请求IP"` 21 | UserAgent string `json:"userAgent" gorm:"comment:用户代理"` 22 | Status int `json:"status" gorm:"comment:响应状态码"` 23 | ErrorMessage string `json:"errorMessage" gorm:"comment:错误信息"` 24 | Latency int64 `json:"latency" gorm:"comment:请求耗时(毫秒)"` 25 | OperationTime time.Time `json:"operationTime" gorm:"comment:操作时间"` 26 | } 27 | 28 | func (SysOperationLog) TableName() string { 29 | return "sys_operation_logs" 30 | } 31 | 32 | // OperationLogRequest 操作日志查询请求 33 | type OperationLogRequest struct { 34 | PageInfo 35 | UserID uint `json:"userId" form:"userId"` 36 | Username string `json:"username" form:"username"` 37 | Method string `json:"method" form:"method"` 38 | Path string `json:"path" form:"path"` 39 | OperationType string `json:"operationType" form:"operationType"` 40 | Status int `json:"status" form:"status"` 41 | StartTime string `json:"startTime" form:"startTime"` 42 | EndTime string `json:"endTime" form:"endTime"` 43 | } 44 | 45 | // OperationLogResponse 操作日志响应 46 | type OperationLogResponse struct { 47 | List []SysOperationLog `json:"list"` 48 | Total int64 `json:"total"` 49 | Page int `json:"page"` 50 | PageSize int `json:"pageSize"` 51 | } 52 | 53 | // PageInfo 分页信息 54 | type PageInfo struct { 55 | Page int `json:"page" form:"page"` 56 | PageSize int `json:"pageSize" form:"pageSize"` 57 | } 58 | -------------------------------------------------------------------------------- /web/src/style/index.scss: -------------------------------------------------------------------------------- 1 | // 全局样式 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | html, 9 | body { 10 | height: 100%; 11 | width: 100%; 12 | font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; 13 | } 14 | 15 | #app { 16 | height: 100%; 17 | width: 100%; 18 | } 19 | 20 | // 滚动条样式 21 | ::-webkit-scrollbar { 22 | width: 6px; 23 | height: 6px; 24 | } 25 | 26 | ::-webkit-scrollbar-track { 27 | background: #f1f1f1; 28 | } 29 | 30 | ::-webkit-scrollbar-thumb { 31 | background: #c1c1c1; 32 | border-radius: 3px; 33 | } 34 | 35 | ::-webkit-scrollbar-thumb:hover { 36 | background: #a8a8a8; 37 | } 38 | 39 | // Element Plus 样式覆盖 40 | .el-menu-item.is-active { 41 | background-color: #409eff !important; 42 | color: #ffffff !important; 43 | 44 | .el-icon { 45 | color: #ffffff !important; 46 | } 47 | 48 | span { 49 | color: #ffffff !important; 50 | } 51 | } 52 | 53 | .el-table { 54 | .el-table__header { 55 | th { 56 | background-color: #fafafa; 57 | color: #606266; 58 | font-weight: 500; 59 | } 60 | } 61 | } 62 | 63 | .el-card { 64 | border-radius: 8px; 65 | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); 66 | } 67 | 68 | .el-dialog { 69 | border-radius: 8px; 70 | } 71 | 72 | // 工具类 73 | .text-center { 74 | text-align: center; 75 | } 76 | 77 | .text-right { 78 | text-align: right; 79 | } 80 | 81 | .text-left { 82 | text-align: left; 83 | } 84 | 85 | .mb-10 { 86 | margin-bottom: 10px; 87 | } 88 | 89 | .mb-20 { 90 | margin-bottom: 20px; 91 | } 92 | 93 | .mt-10 { 94 | margin-top: 10px; 95 | } 96 | 97 | .mt-20 { 98 | margin-top: 20px; 99 | } 100 | 101 | .ml-10 { 102 | margin-left: 10px; 103 | } 104 | 105 | .mr-10 { 106 | margin-right: 10px; 107 | } 108 | 109 | .p-10 { 110 | padding: 10px; 111 | } 112 | 113 | .p-20 { 114 | padding: 20px; 115 | } 116 | 117 | .flex { 118 | display: flex; 119 | } 120 | 121 | .flex-center { 122 | display: flex; 123 | align-items: center; 124 | justify-content: center; 125 | } 126 | 127 | .flex-between { 128 | display: flex; 129 | align-items: center; 130 | justify-content: space-between; 131 | } 132 | 133 | .w-100 { 134 | width: 100%; 135 | } 136 | 137 | .h-100 { 138 | height: 100%; 139 | } 140 | 141 | // 确保页面内容充满宽度 142 | .el-row { 143 | width: 100%; 144 | } 145 | 146 | .el-col { 147 | width: 100%; 148 | } 149 | 150 | // 页面容器样式 151 | .page-container { 152 | width: 100%; 153 | max-width: 100%; 154 | padding: 20px; 155 | box-sizing: border-box; 156 | } -------------------------------------------------------------------------------- /server/api/v1/base.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "net/http" 5 | "server/global" 6 | "server/model/system" 7 | "server/utils" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // LoginRequest 登录请求结构体 13 | type LoginRequest struct { 14 | Username string `json:"username" binding:"required"` 15 | Password string `json:"password" binding:"required"` 16 | } 17 | 18 | // LoginResponse 登录响应结构体 19 | type LoginResponse struct { 20 | User system.SysUser `json:"user"` 21 | Token string `json:"token"` 22 | } 23 | 24 | // Login 用户登录 25 | func Login(c *gin.Context) { 26 | var req LoginRequest 27 | if err := c.ShouldBindJSON(&req); err != nil { 28 | c.JSON(http.StatusBadRequest, gin.H{ 29 | "code": 400, 30 | "msg": "参数错误: " + err.Error(), 31 | }) 32 | return 33 | } 34 | 35 | // 查找用户 36 | var user system.SysUser 37 | if err := global.DB.Where("username = ?", req.Username).First(&user).Error; err != nil { 38 | c.JSON(http.StatusUnauthorized, gin.H{ 39 | "code": 401, 40 | "msg": "用户名或密码错误", 41 | }) 42 | return 43 | } 44 | 45 | // 验证密码 46 | if !checkPassword(req.Password, user.Password) { 47 | c.JSON(http.StatusUnauthorized, gin.H{ 48 | "code": 401, 49 | "msg": "用户名或密码错误", 50 | }) 51 | return 52 | } 53 | 54 | // 检查用户是否被禁用 55 | if user.Enable != 1 { 56 | c.JSON(http.StatusUnauthorized, gin.H{ 57 | "code": 401, 58 | "msg": "用户已被禁用", 59 | }) 60 | return 61 | } 62 | 63 | // 生成JWT token 64 | token, err := utils.GenerateToken(user.ID, user.Username, user.AuthorityId) 65 | if err != nil { 66 | c.JSON(http.StatusInternalServerError, gin.H{ 67 | "code": 500, 68 | "msg": "生成token失败", 69 | }) 70 | return 71 | } 72 | 73 | // 清除密码字段 74 | user.Password = "" 75 | 76 | c.JSON(http.StatusOK, gin.H{ 77 | "code": 0, 78 | "msg": "登录成功", 79 | "data": LoginResponse{ 80 | User: user, 81 | Token: token, 82 | }, 83 | }) 84 | } 85 | 86 | // checkPassword 验证密码 87 | func checkPassword(password, hashedPassword string) bool { 88 | return utils.BcryptCheck(password, hashedPassword) 89 | } 90 | 91 | // Logout 用户登出 92 | func Logout(c *gin.Context) { 93 | c.JSON(http.StatusOK, gin.H{ 94 | "code": 0, 95 | "msg": "登出成功", 96 | }) 97 | } 98 | 99 | // Captcha 获取验证码(暂时返回固定值) 100 | func Captcha(c *gin.Context) { 101 | c.JSON(http.StatusOK, gin.H{ 102 | "code": 0, 103 | "msg": "获取成功", 104 | "data": gin.H{ 105 | "captchaId": "captcha_" + utils.RandomString(8), 106 | "pictureBases64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", 107 | }, 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /server/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/casbin/casbin/v2 v2.81.0 7 | github.com/fsnotify/fsnotify v1.7.0 8 | github.com/gin-contrib/cors v1.5.0 9 | github.com/gin-gonic/gin v1.9.1 10 | github.com/go-redis/redis/v8 v8.11.5 11 | github.com/golang-jwt/jwt/v4 v4.5.2 12 | github.com/google/uuid v1.5.0 13 | github.com/spf13/viper v1.18.2 14 | gorm.io/driver/mysql v1.5.2 15 | gorm.io/gorm v1.25.5 16 | ) 17 | 18 | require ( 19 | github.com/bytedance/sonic v1.10.1 // indirect 20 | github.com/casbin/govaluate v1.1.0 // indirect 21 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 22 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 23 | github.com/chenzhuoyu/iasm v0.9.0 // indirect 24 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 25 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 26 | github.com/gin-contrib/sse v0.1.0 // indirect 27 | github.com/go-playground/locales v0.14.1 // indirect 28 | github.com/go-playground/universal-translator v0.18.1 // indirect 29 | github.com/go-playground/validator/v10 v10.15.5 // indirect 30 | github.com/go-sql-driver/mysql v1.7.0 // indirect 31 | github.com/goccy/go-json v0.10.2 // indirect 32 | github.com/hashicorp/hcl v1.0.0 // indirect 33 | github.com/jinzhu/inflection v1.0.0 // indirect 34 | github.com/jinzhu/now v1.1.5 // indirect 35 | github.com/json-iterator/go v1.1.12 // indirect 36 | github.com/klauspost/cpuid/v2 v2.2.5 // indirect 37 | github.com/leodido/go-urn v1.2.4 // indirect 38 | github.com/magiconair/properties v1.8.7 // indirect 39 | github.com/mattn/go-isatty v0.0.19 // indirect 40 | github.com/mitchellh/mapstructure v1.5.0 // indirect 41 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 42 | github.com/modern-go/reflect2 v1.0.2 // indirect 43 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 44 | github.com/sagikazarmark/locafero v0.4.0 // indirect 45 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 46 | github.com/sourcegraph/conc v0.3.0 // indirect 47 | github.com/spf13/afero v1.11.0 // indirect 48 | github.com/spf13/cast v1.6.0 // indirect 49 | github.com/spf13/pflag v1.0.5 // indirect 50 | github.com/subosito/gotenv v1.6.0 // indirect 51 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 52 | github.com/ugorji/go/codec v1.2.11 // indirect 53 | go.uber.org/atomic v1.9.0 // indirect 54 | go.uber.org/multierr v1.9.0 // indirect 55 | golang.org/x/arch v0.5.0 // indirect 56 | golang.org/x/crypto v0.17.0 // indirect 57 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 58 | golang.org/x/net v0.19.0 // indirect 59 | golang.org/x/sys v0.15.0 // indirect 60 | golang.org/x/text v0.14.0 // indirect 61 | google.golang.org/protobuf v1.31.0 // indirect 62 | gopkg.in/ini.v1 v1.67.0 // indirect 63 | gopkg.in/yaml.v3 v3.0.1 // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /web/src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 70 | 71 | -------------------------------------------------------------------------------- /server/api/v1/upload.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "mime/multipart" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/google/uuid" 15 | ) 16 | 17 | const ( 18 | MaxFileSize = 10 * 1024 * 1024 // 10MB 19 | UploadPath = "./uploads/avatar/" 20 | ) 21 | 22 | // UploadAvatar 上传头像 23 | func UploadAvatar(c *gin.Context) { 24 | // 限制文件大小为10MB 25 | c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, MaxFileSize) 26 | 27 | file, header, err := c.Request.FormFile("file") 28 | if err != nil { 29 | if err.Error() == "http: request body too large" { 30 | c.JSON(http.StatusBadRequest, gin.H{ 31 | "code": 400, 32 | "msg": "文件大小不能超过10MB", 33 | }) 34 | return 35 | } 36 | c.JSON(http.StatusBadRequest, gin.H{ 37 | "code": 400, 38 | "msg": "请选择要上传的文件", 39 | }) 40 | return 41 | } 42 | defer file.Close() 43 | 44 | // 验证文件类型 45 | if !isValidImageType(header) { 46 | c.JSON(http.StatusBadRequest, gin.H{ 47 | "code": 400, 48 | "msg": "只支持上传 JPG、JPEG、PNG、GIF 格式的图片", 49 | }) 50 | return 51 | } 52 | 53 | // 验证文件大小 54 | if header.Size > MaxFileSize { 55 | c.JSON(http.StatusBadRequest, gin.H{ 56 | "code": 400, 57 | "msg": "文件大小不能超过10MB", 58 | }) 59 | return 60 | } 61 | 62 | // 创建上传目录 63 | if err := os.MkdirAll(UploadPath, 0755); err != nil { 64 | c.JSON(http.StatusInternalServerError, gin.H{ 65 | "code": 500, 66 | "msg": "创建上传目录失败", 67 | }) 68 | return 69 | } 70 | 71 | // 生成新的文件名 72 | ext := filepath.Ext(header.Filename) 73 | newFileName := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext) 74 | filePath := filepath.Join(UploadPath, newFileName) 75 | 76 | // 保存文件 77 | dst, err := os.Create(filePath) 78 | if err != nil { 79 | c.JSON(http.StatusInternalServerError, gin.H{ 80 | "code": 500, 81 | "msg": "保存文件失败", 82 | }) 83 | return 84 | } 85 | defer dst.Close() 86 | 87 | if _, err := io.Copy(dst, file); err != nil { 88 | c.JSON(http.StatusInternalServerError, gin.H{ 89 | "code": 500, 90 | "msg": "保存文件失败", 91 | }) 92 | return 93 | } 94 | 95 | // 生成访问URL 96 | fileURL := fmt.Sprintf("/uploads/avatar/%s", newFileName) 97 | 98 | c.JSON(http.StatusOK, gin.H{ 99 | "code": 0, 100 | "msg": "上传成功", 101 | "data": gin.H{ 102 | "url": fileURL, 103 | "filename": newFileName, 104 | }, 105 | }) 106 | } 107 | 108 | // isValidImageType 验证是否为有效的图片类型 109 | func isValidImageType(header *multipart.FileHeader) bool { 110 | allowedTypes := map[string]bool{ 111 | "image/jpeg": true, 112 | "image/jpg": true, 113 | "image/png": true, 114 | "image/gif": true, 115 | } 116 | 117 | // 检查Content-Type 118 | contentType := header.Header.Get("Content-Type") 119 | if allowedTypes[contentType] { 120 | return true 121 | } 122 | 123 | // 检查文件扩展名 124 | ext := strings.ToLower(filepath.Ext(header.Filename)) 125 | allowedExts := map[string]bool{ 126 | ".jpg": true, 127 | ".jpeg": true, 128 | ".png": true, 129 | ".gif": true, 130 | } 131 | 132 | return allowedExts[ext] 133 | } 134 | -------------------------------------------------------------------------------- /web/src/utils/route.js: -------------------------------------------------------------------------------- 1 | // 动态路由工具 2 | import router from '@/router' 3 | 4 | // 组件映射表,用于动态导入组件 5 | const componentMap = { 6 | // 布局组件 7 | 'layout/index.vue': () => import('@/layout/index.vue'), 8 | 9 | // 页面组件 10 | 'views/dashboard/index.vue': () => import('@/views/dashboard/index.vue'), 11 | 'views/profile/index.vue': () => import('@/views/profile/index.vue'), 12 | 'views/system/user/index.vue': () => import('@/views/system/user/index.vue'), 13 | 'views/system/role/index.vue': () => import('@/views/system/role/index.vue'), 14 | 'views/system/role/permission.vue': () => import('@/views/system/role/permission.vue'), 15 | 'views/system/menu/index.vue': () => import('@/views/system/menu/index.vue'), 16 | 'views/system/operation-log/index.vue': () => import('@/views/system/operation-log/index.vue'), 17 | 18 | // 错误页面 19 | 'views/error/404.vue': () => import('@/views/error/404.vue'), 20 | } 21 | 22 | /** 23 | * 将后端菜单数据转换为前端路由格式 24 | */ 25 | export function transformMenusToRoutes(menus) { 26 | const routes = [] 27 | 28 | for (const menu of menus) { 29 | // 只处理菜单类型,跳过按钮类型 30 | if (menu.menuType === 'button' || menu.hidden) { 31 | continue 32 | } 33 | 34 | const route = { 35 | path: menu.path, 36 | name: menu.name, 37 | component: getComponent(menu.component), 38 | meta: { 39 | title: menu.title, 40 | icon: menu.icon, 41 | keepAlive: menu.keepAlive, 42 | requiresAuth: true 43 | } 44 | } 45 | 46 | // 处理子路由 47 | if (menu.children && menu.children.length > 0) { 48 | const childRoutes = transformMenusToRoutes(menu.children) 49 | if (childRoutes.length > 0) { 50 | route.children = childRoutes 51 | } 52 | } 53 | 54 | routes.push(route) 55 | } 56 | 57 | return routes 58 | } 59 | 60 | /** 61 | * 根据组件路径获取组件 62 | */ 63 | function getComponent(componentPath) { 64 | if (!componentPath) { 65 | return () => import('@/views/error/404.vue') 66 | } 67 | 68 | // 从组件映射表中获取组件 69 | const component = componentMap[componentPath] 70 | if (component) { 71 | return component 72 | } 73 | 74 | // 如果映射表中没有,尝试动态导入 75 | console.warn(`组件 ${componentPath} 未在映射表中找到,尝试动态导入`) 76 | return () => import(`@/${componentPath}`).catch(() => { 77 | console.error(`无法加载组件: ${componentPath}`) 78 | return import('@/views/error/404.vue') 79 | }) 80 | } 81 | 82 | /** 83 | * 动态添加路由 84 | */ 85 | export function addDynamicRoutes(menus) { 86 | const routes = transformMenusToRoutes(menus) 87 | 88 | // 清除之前添加的动态路由(如果需要的话) 89 | // 注意:Vue Router 4 没有直接的清除路由方法,需要记录添加的路由名称 90 | 91 | // 添加动态路由 92 | routes.forEach(route => { 93 | try { 94 | router.addRoute(route) 95 | console.log(`动态添加路由: ${route.path}`) 96 | } catch (error) { 97 | console.error(`添加路由失败: ${route.path}`, error) 98 | } 99 | }) 100 | 101 | return routes 102 | } 103 | 104 | /** 105 | * 重置动态路由 106 | */ 107 | export function resetDynamicRoutes() { 108 | // 由于 Vue Router 4 没有直接的移除路由方法 109 | // 我们需要重新创建 router 实例或者记录动态路由进行管理 110 | // 这里采用简单的方式:重新加载页面来重置路由 111 | console.log('重置动态路由') 112 | } -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Go-Gin-Element-Admin 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 |
74 | 系统加载中 75 |
76 |
77 | 78 | 79 | 84 | 85 | 86 | 87 | 88 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-gin-element-admin 2 | 3 | 基于 Go + Gin + MySQL8 + GORM + Redis + Vue3 + Element-Plus 构建的极简后端管理系统脚手架 4 | 5 | ## 技术栈 6 | 7 | ### 后端 8 | - **Go 1.21** - 编程语言 9 | - **Gin** - Web 框架 10 | - **GORM** - ORM 框架 11 | - **MySQL 8** - 数据库 12 | - **Redis** - 缓存 13 | - **Casbin** - 权限管理 14 | - **JWT** - 身份认证 15 | - **Viper** - 配置管理 16 | 17 | ### 前端 18 | - **Vue 3** - 前端框架 19 | - **Element Plus** - UI 组件库 20 | - **Vite** - 构建工具 21 | - **Pinia** - 状态管理 22 | - **Vue Router** - 路由管理 23 | - **Axios** - HTTP 客户端 24 | 25 | ## 功能特性 26 | 27 | - 🔐 **用户认证** - JWT 登录认证 28 | - 👤 **个人中心** - 个人信息管理、密码修改、头像上传 29 | - 🏠 **仪表板** - 系统概览、数据统计、快捷操作 30 | - 👥 **用户管理** - 用户增删改查、状态管理 31 | - 🛡️ **角色管理** - 角色权限管理、基于 RBAC 的权限控制 32 | - 📋 **菜单管理** - 动态菜单配置、权限分配 33 | - 📊 **操作日志** - 详细的操作审计、实时监控、数据统计 34 | - 🎨 **现代化 UI** - 响应式设计、美观的界面 35 | 36 | ## 项目结构 37 | 38 | ``` 39 | go-gin-element-admin/ 40 | ├── server/ # 后端代码 41 | │ ├── main.go # 程序入口 42 | │ ├── config/ # 配置文件 43 | │ ├── core/ # 核心功能 44 | │ ├── global/ # 全局变量 45 | │ ├── initialize/ # 初始化 46 | │ └── model/ # 数据模型 47 | ├── web/ # 前端代码 48 | │ ├── src/ 49 | │ │ ├── api/ # API 接口 50 | │ │ ├── components/ # 组件 51 | │ │ ├── layout/ # 布局 52 | │ │ ├── router/ # 路由 53 | │ │ ├── stores/ # 状态管理 54 | │ │ ├── utils/ # 工具函数 55 | │ │ └── views/ # 页面 56 | │ ├── package.json 57 | │ └── vite.config.js 58 | └── README.md 59 | ``` 60 | 61 | ## 快速开始 62 | 63 | ### 环境要求 64 | 65 | - Go 1.21+ 66 | - Node.js 16+ 67 | - MySQL 8.0+ 68 | - Redis 6.0+ 69 | 70 | ### 后端启动 71 | 72 | 1. 进入后端目录 73 | ```bash 74 | cd server 75 | ``` 76 | 77 | 2. 安装依赖 78 | ```bash 79 | go mod tidy 80 | ``` 81 | 82 | 3. 配置数据库 83 | 编辑 `server/config/config.yaml` 文件,修改数据库连接信息: 84 | ```yaml 85 | mysql: 86 | path: '127.0.0.1:3306' 87 | db-name: 'go_gin_element_admin' 88 | username: 'root' 89 | password: 'your_password' 90 | ``` 91 | 92 | 4. 启动服务 93 | ```bash 94 | go run main.go 95 | ``` 96 | 97 | 后端服务将在 `http://localhost:8888` 启动 98 | 99 | ### 前端启动 100 | 101 | 1. 进入前端目录 102 | ```bash 103 | cd web 104 | ``` 105 | 106 | 2. 安装依赖 107 | ```bash 108 | npm install 109 | ``` 110 | 111 | 3. 启动开发服务器 112 | ```bash 113 | npm run dev 114 | ``` 115 | 116 | 前端服务将在 `http://localhost:8080` 启动 117 | 118 | ### 默认账号 119 | 120 | - 用户名:`admin` 121 | - 密码:`123456` 122 | 123 | > 注意:请修改 `server/config/config.yaml` 中的数据库密码为你的实际密码 124 | 125 | ## 开发说明 126 | 127 | ### 后端开发 128 | 129 | - 配置文件位于 `server/config/config.yaml` 130 | - 数据模型定义在 `server/model/` 目录 131 | - API 路由配置在 `server/router/` 目录 132 | - 业务逻辑在 `server/service/` 目录 133 | 134 | ### 前端开发 135 | 136 | - 页面组件在 `web/src/views/` 目录 137 | - API 接口定义在 `web/src/api/` 目录 138 | - 路由配置在 `web/src/router/index.js` 139 | - 状态管理在 `web/src/stores/` 目录 140 | 141 | ## 部署 142 | 143 | ### 后端部署 144 | 145 | 1. 编译 146 | ```bash 147 | cd server 148 | go build -o app main.go 149 | ``` 150 | 151 | 2. 运行 152 | ```bash 153 | ./app 154 | ``` 155 | 156 | ### 前端部署 157 | 158 | 1. 构建 159 | ```bash 160 | cd web 161 | npm run build 162 | ``` 163 | 164 | 2. 部署 `dist` 目录到 Web 服务器 165 | 166 | ## 贡献 167 | 168 | 欢迎提交 Issue 和 Pull Request! 169 | 170 | ## 许可证 171 | 172 | MIT License 173 | -------------------------------------------------------------------------------- /server/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Server struct { 4 | JWT JWT `mapstructure:"jwt" json:"jwt" yaml:"jwt"` 5 | Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"` 6 | Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"` 7 | Casbin Casbin `mapstructure:"casbin" json:"casbin" yaml:"casbin"` 8 | System System `mapstructure:"system" json:"system" yaml:"system"` 9 | Mysql Mysql `mapstructure:"mysql" json:"mysql" yaml:"mysql"` 10 | CORS CORS `mapstructure:"cors" json:"cors" yaml:"cors"` 11 | } 12 | 13 | type System struct { 14 | Env string `mapstructure:"env" json:"env" yaml:"env"` 15 | Addr int `mapstructure:"addr" json:"addr" yaml:"addr"` 16 | DbType string `mapstructure:"db-type" json:"db-type" yaml:"db-type"` 17 | UseMultipoint bool `mapstructure:"use-multipoint" json:"use-multipoint" yaml:"use-multipoint"` 18 | UseRedis bool `mapstructure:"use-redis" json:"use-redis" yaml:"use-redis"` 19 | } 20 | 21 | type JWT struct { 22 | SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` 23 | ExpiresTime int64 `mapstructure:"expires-time" json:"expires-time" yaml:"expires-time"` 24 | BufferTime int64 `mapstructure:"buffer-time" json:"buffer-time" yaml:"buffer-time"` 25 | Issuer string `mapstructure:"issuer" json:"issuer" yaml:"issuer"` 26 | } 27 | 28 | type Casbin struct { 29 | ModelPath string `mapstructure:"model-path" json:"model-path" yaml:"model-path"` 30 | } 31 | 32 | type Mysql struct { 33 | Path string `mapstructure:"path" json:"path" yaml:"path"` 34 | DbName string `mapstructure:"db-name" json:"db-name" yaml:"db-name"` 35 | Username string `mapstructure:"username" json:"username" yaml:"username"` 36 | Password string `mapstructure:"password" json:"password" yaml:"password"` 37 | Port string `mapstructure:"port" json:"port" yaml:"port"` 38 | Config string `mapstructure:"config" json:"config" yaml:"config"` 39 | MaxIdleConns int `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns"` 40 | MaxOpenConns int `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns"` 41 | LogMode string `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode"` 42 | LogZap bool `mapstructure:"log-zap" json:"log-zap" yaml:"log-zap"` 43 | } 44 | 45 | type Redis struct { 46 | DB int `mapstructure:"db" json:"db" yaml:"db"` 47 | Addr string `mapstructure:"addr" json:"addr" yaml:"addr"` 48 | Password string `mapstructure:"password" json:"password" yaml:"password"` 49 | } 50 | 51 | type Zap struct { 52 | Level string `mapstructure:"level" json:"level" yaml:"level"` 53 | Format string `mapstructure:"format" json:"format" yaml:"format"` 54 | Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` 55 | Director string `mapstructure:"director" json:"director" yaml:"director"` 56 | ShowLine bool `mapstructure:"show-line" json:"show-line" yaml:"show-line"` 57 | EncodeLevel string `mapstructure:"encode-level" json:"encode-level" yaml:"encode-level"` 58 | StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktrace-key" yaml:"stacktrace-key"` 59 | LogInConsole bool `mapstructure:"log-in-console" json:"log-in-console" yaml:"log-in-console"` 60 | } 61 | 62 | type CORS struct { 63 | Mode string `mapstructure:"mode" json:"mode" yaml:"mode"` 64 | } 65 | -------------------------------------------------------------------------------- /web/src/stores/user.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | import { login as loginApi } from '@/api/user' 4 | import { getUserInfo as getUserInfoApi, getUserMenus as getUserMenusApi } from '@/api/system/user' 5 | import { getToken, setToken, removeToken } from '@/utils/auth' 6 | import router from '@/router' 7 | 8 | export const useUserStore = defineStore('user', () => { 9 | const token = ref(getToken()) 10 | const userInfo = ref({}) 11 | const userMenus = ref([]) 12 | const userPermissions = ref([]) 13 | 14 | // 登录 15 | const login = async (loginForm) => { 16 | try { 17 | const response = await loginApi(loginForm) 18 | if (response.code === 0) { 19 | token.value = response.data.token 20 | setToken(response.data.token) 21 | return response 22 | } else { 23 | throw new Error(response.msg) 24 | } 25 | } catch (error) { 26 | throw error 27 | } 28 | } 29 | 30 | // 获取用户信息 31 | const getUserInfo = async () => { 32 | try { 33 | const response = await getUserInfoApi() 34 | if (response.code === 0) { 35 | userInfo.value = response.data 36 | return response.data 37 | } else { 38 | throw new Error(response.msg) 39 | } 40 | } catch (error) { 41 | throw error 42 | } 43 | } 44 | 45 | // 获取用户菜单 46 | const getUserMenus = async () => { 47 | try { 48 | const response = await getUserMenusApi() 49 | if (response.code === 0) { 50 | const menuData = response.data || [] 51 | userMenus.value = menuData 52 | 53 | // 提取权限编码 54 | const permissions = extractPermissions(menuData) 55 | userPermissions.value = permissions 56 | 57 | return menuData 58 | } else { 59 | throw new Error(response.msg) 60 | } 61 | } catch (error) { 62 | throw error 63 | } 64 | } 65 | 66 | // 提取权限编码的递归函数 67 | const extractPermissions = (menus) => { 68 | const permissions = [] 69 | 70 | const traverse = (items) => { 71 | for (const item of items) { 72 | // 如果有权限编码,添加到权限列表 73 | if (item.permissionCode && item.permissionCode.trim()) { 74 | permissions.push(item.permissionCode.trim()) 75 | } 76 | // 递归处理子菜单 77 | if (item.children && item.children.length > 0) { 78 | traverse(item.children) 79 | } 80 | } 81 | } 82 | 83 | traverse(menus) 84 | 85 | // 去重 86 | return [...new Set(permissions)] 87 | } 88 | 89 | // 检查登录状态 90 | const checkLogin = () => { 91 | const savedToken = getToken() 92 | if (savedToken && savedToken.trim()) { 93 | token.value = savedToken 94 | // 如果有token但没有用户信息,获取用户信息和菜单 95 | if (!userInfo.value.id) { 96 | getUserInfo().then(() => { 97 | getUserMenus() 98 | }).catch(() => { 99 | // 如果获取失败,说明token无效,清空状态 100 | logout() 101 | }) 102 | } 103 | } 104 | } 105 | 106 | // 退出登录 107 | const logout = () => { 108 | token.value = '' 109 | userInfo.value = {} 110 | userMenus.value = [] 111 | userPermissions.value = [] 112 | removeToken() 113 | router.push('/login') 114 | } 115 | 116 | return { 117 | token, 118 | userInfo, 119 | userMenus, 120 | userPermissions, 121 | login, 122 | getUserInfo, 123 | getUserMenus, 124 | checkLogin, 125 | logout 126 | } 127 | }) -------------------------------------------------------------------------------- /web/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import { useUserStore } from '@/stores/user' 3 | import NProgress from 'nprogress' 4 | import 'nprogress/nprogress.css' 5 | 6 | // 配置 NProgress 7 | NProgress.configure({ showSpinner: false }) 8 | 9 | const routes = [ 10 | { 11 | path: '/login', 12 | name: 'Login', 13 | component: () => import('@/views/login/index.vue'), 14 | meta: { 15 | title: '登录', 16 | requiresAuth: false 17 | } 18 | }, 19 | { 20 | path: '/', 21 | redirect: '/dashboard', 22 | component: () => import('@/layout/index.vue'), 23 | meta: { 24 | requiresAuth: true 25 | }, 26 | children: [ 27 | { 28 | path: 'dashboard', 29 | name: 'Dashboard', 30 | component: () => import('@/views/dashboard/index.vue'), 31 | meta: { 32 | title: '首页', 33 | icon: 'House' 34 | } 35 | }, 36 | { 37 | path: 'profile', 38 | name: 'Profile', 39 | component: () => import('@/views/profile/index.vue'), 40 | meta: { 41 | title: '个人中心', 42 | icon: 'User' 43 | } 44 | } 45 | ] 46 | }, 47 | { 48 | path: '/system', 49 | name: 'System', 50 | component: () => import('@/layout/index.vue'), 51 | meta: { 52 | title: '系统设置', 53 | icon: 'Setting', 54 | requiresAuth: true 55 | }, 56 | children: [ 57 | { 58 | path: 'user', 59 | name: 'SystemUser', 60 | component: () => import('@/views/system/user/index.vue'), 61 | meta: { 62 | title: '人员管理', 63 | icon: 'User' 64 | } 65 | }, 66 | { 67 | path: 'role', 68 | name: 'SystemRole', 69 | component: () => import('@/views/system/role/index.vue'), 70 | meta: { 71 | title: '角色管理', 72 | icon: 'UserFilled' 73 | } 74 | }, 75 | { 76 | path: 'role/:id/permission', 77 | name: 'RolePermission', 78 | component: () => import('@/views/system/role/permission.vue'), 79 | meta: { 80 | title: '权限管理', 81 | icon: 'Lock', 82 | hidden: true 83 | } 84 | }, 85 | { 86 | path: 'menu', 87 | name: 'SystemMenu', 88 | component: () => import('@/views/system/menu/index.vue'), 89 | meta: { 90 | title: '菜单管理', 91 | icon: 'Menu' 92 | } 93 | }, 94 | { 95 | path: 'operation-log', 96 | name: 'SystemOperationLog', 97 | component: () => import('@/views/system/operation-log/index.vue'), 98 | meta: { 99 | title: '操作日志', 100 | icon: 'Document' 101 | } 102 | } 103 | ] 104 | }, 105 | { 106 | path: '/:pathMatch(.*)*', 107 | name: 'NotFound', 108 | component: () => import('@/views/error/404.vue') 109 | } 110 | ] 111 | 112 | const router = createRouter({ 113 | history: createWebHistory(), 114 | routes 115 | }) 116 | 117 | // 路由守卫 118 | router.beforeEach(async (to, from, next) => { 119 | NProgress.start() 120 | 121 | const userStore = useUserStore() 122 | const token = userStore.token 123 | 124 | if (to.path === '/login') { 125 | if (token) { 126 | next('/') 127 | } else { 128 | next() 129 | } 130 | } else { 131 | if (token) { 132 | if (!userStore.userInfo.id) { 133 | try { 134 | await userStore.getUserInfo() 135 | // 获取用户信息成功后,获取用户菜单 136 | await userStore.getUserMenus() 137 | next() 138 | } catch (error) { 139 | userStore.logout() 140 | next('/login') 141 | } 142 | } else { 143 | // 如果用户信息存在但菜单为空,尝试获取菜单 144 | if (!userStore.userMenus || userStore.userMenus.length === 0) { 145 | try { 146 | await userStore.getUserMenus() 147 | } catch (error) { 148 | console.warn('获取用户菜单失败:', error) 149 | } 150 | } 151 | next() 152 | } 153 | } else { 154 | next('/login') 155 | } 156 | } 157 | }) 158 | 159 | router.afterEach(() => { 160 | NProgress.done() 161 | }) 162 | 163 | export default router -------------------------------------------------------------------------------- /web/src/utils/permission.js: -------------------------------------------------------------------------------- 1 | import { useUserStore } from '@/stores/user' 2 | 3 | /** 4 | * 检查用户是否有指定权限 5 | * @param {string|Array} permission - 权限编码或权限编码数组 6 | * @param {string} mode - 检查模式:'some'(任一权限) 或 'every'(全部权限),默认'some' 7 | * @returns {boolean} 是否有权限 8 | */ 9 | export function hasPermission(permission, mode = 'some') { 10 | const userStore = useUserStore() 11 | const userPermissions = userStore.userPermissions || [] 12 | const userInfo = userStore.userInfo || {} 13 | 14 | if (!permission) return true 15 | 16 | // 详细的调试信息 17 | console.log('=== 权限检查开始 ===') 18 | console.log('检查权限:', permission) 19 | console.log('用户信息:', { 20 | username: userInfo.username, 21 | authorityId: userInfo.authority?.authorityId, 22 | id: userInfo.id 23 | }) 24 | console.log('用户权限列表:', userPermissions) 25 | console.log('权限列表长度:', userPermissions.length) 26 | 27 | // 管理员检查 28 | const isAdmin = userInfo?.username === 'admin' || userInfo?.authority?.authorityId === 888 29 | console.log('是否管理员:', isAdmin) 30 | 31 | if (isAdmin) { 32 | console.log('管理员用户,直接返回true') 33 | return true 34 | } 35 | 36 | // 如果是字符串,转为数组 37 | const permissions = Array.isArray(permission) ? permission : [permission] 38 | console.log('需要检查的权限:', permissions) 39 | 40 | let hasAuth = false 41 | if (mode === 'every') { 42 | // 检查是否拥有所有权限 43 | hasAuth = permissions.every(perm => { 44 | const has = userPermissions.includes(perm) 45 | console.log(`权限 ${perm}:`, has ? '✓' : '✗') 46 | return has 47 | }) 48 | } else { 49 | // 检查是否拥有任一权限 50 | hasAuth = permissions.some(perm => { 51 | const has = userPermissions.includes(perm) 52 | console.log(`权限 ${perm}:`, has ? '✓' : '✗') 53 | return has 54 | }) 55 | } 56 | 57 | console.log('最终结果:', hasAuth ? '有权限' : '无权限') 58 | console.log('=== 权限检查结束 ===') 59 | 60 | return hasAuth 61 | } 62 | 63 | /** 64 | * 权限指令 65 | * 用法:v-permission="'user:create'" 或 v-permission="['user:create', 'user:update']" 66 | */ 67 | export const permissionDirective = { 68 | mounted(el, binding) { 69 | const { value } = binding 70 | 71 | if (!hasPermission(value)) { 72 | // 如果没有权限,隐藏元素 73 | el.style.display = 'none' 74 | // 或者完全移除元素 75 | // el.parentNode && el.parentNode.removeChild(el) 76 | } 77 | }, 78 | 79 | updated(el, binding) { 80 | const { value } = binding 81 | 82 | if (!hasPermission(value)) { 83 | el.style.display = 'none' 84 | } else { 85 | el.style.display = '' 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * 角色权限指令 92 | * 用法:v-role="'admin'" 或 v-role="['admin', 'user']" 93 | */ 94 | export const roleDirective = { 95 | mounted(el, binding) { 96 | const { value } = binding 97 | const userStore = useUserStore() 98 | const userRoles = userStore.userInfo?.roles || [] 99 | 100 | if (!value) return 101 | 102 | const roles = Array.isArray(value) ? value : [value] 103 | const hasRole = roles.some(role => userRoles.includes(role)) 104 | 105 | if (!hasRole) { 106 | el.style.display = 'none' 107 | } 108 | }, 109 | 110 | updated(el, binding) { 111 | const { value } = binding 112 | const userStore = useUserStore() 113 | const userRoles = userStore.userInfo?.roles || [] 114 | 115 | if (!value) return 116 | 117 | const roles = Array.isArray(value) ? value : [value] 118 | const hasRole = roles.some(role => userRoles.includes(role)) 119 | 120 | if (!hasRole) { 121 | el.style.display = 'none' 122 | } else { 123 | el.style.display = '' 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * 超级管理员检查 130 | * @returns {boolean} 是否为超级管理员 131 | */ 132 | export function isSuperAdmin() { 133 | const userStore = useUserStore() 134 | return userStore.userInfo?.username === 'admin' || userStore.userInfo?.authorityId === 888 135 | } 136 | 137 | /** 138 | * 权限检查装饰器(用于组合式API) 139 | * @param {string|Array} permission - 权限编码 140 | * @param {Function} callback - 有权限时执行的回调 141 | * @param {Function} fallback - 无权限时执行的回调 142 | */ 143 | export function withPermission(permission, callback, fallback) { 144 | if (hasPermission(permission)) { 145 | return callback && callback() 146 | } else { 147 | return fallback && fallback() 148 | } 149 | } -------------------------------------------------------------------------------- /web/src/layout/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 101 | 102 | -------------------------------------------------------------------------------- /server/initialize/router.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | v1 "server/api/v1" 8 | "server/middleware" 9 | 10 | "github.com/gin-contrib/cors" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | // 初始化总路由 15 | func Routers() *gin.Engine { 16 | Router := gin.Default() 17 | 18 | // 跨域配置 19 | Router.Use(cors.New(cors.Config{ 20 | AllowOrigins: []string{"*"}, 21 | AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, 22 | AllowHeaders: []string{"*"}, 23 | ExposeHeaders: []string{"Content-Length", "Authorization"}, 24 | AllowCredentials: true, 25 | })) 26 | 27 | // 操作日志中间件 28 | Router.Use(middleware.OperationLogMiddlewareWithBody()) 29 | 30 | // 健康监测 31 | Router.GET("/health", func(c *gin.Context) { 32 | c.JSON(http.StatusOK, gin.H{ 33 | "message": "ok", 34 | "status": "running", 35 | }) 36 | }) 37 | 38 | // 静态文件服务 - 头像文件访问 39 | Router.Static("/uploads", "./uploads") 40 | 41 | // 公开路由组 42 | PublicGroup := Router.Group("/api") 43 | { 44 | PublicGroup.GET("/health", func(c *gin.Context) { 45 | c.JSON(http.StatusOK, gin.H{ 46 | "message": "ok", 47 | "status": "running", 48 | }) 49 | }) 50 | 51 | // 基础路由(无需认证) 52 | BaseGroup := PublicGroup.Group("/base") 53 | { 54 | BaseGroup.POST("/login", v1.Login) 55 | BaseGroup.POST("/logout", v1.Logout) 56 | BaseGroup.GET("/captcha", v1.Captcha) 57 | } 58 | 59 | // 文件上传路由 60 | UploadGroup := PublicGroup.Group("/upload") 61 | { 62 | UploadGroup.POST("/avatar", v1.UploadAvatar) 63 | } 64 | 65 | // 仪表盘路由 66 | DashboardGroup := PublicGroup.Group("/dashboard") 67 | { 68 | DashboardGroup.GET("/stats", v1.GetDashboardStats) 69 | DashboardGroup.GET("/systemInfo", v1.GetSystemInfo) 70 | } 71 | fmt.Println("Dashboard routes initialized") 72 | 73 | // 系统管理路由 74 | SystemGroup := PublicGroup.Group("/system") 75 | { 76 | // 用户管理 77 | UserGroup := SystemGroup.Group("/user") 78 | { 79 | UserGroup.GET("/list", v1.GetUserList) 80 | UserGroup.GET("/info", middleware.JWTAuth(), v1.GetUserInfo) 81 | UserGroup.GET("/menus", middleware.JWTAuth(), v1.GetUserMenus) 82 | UserGroup.PUT("/info", middleware.JWTAuth(), v1.UpdateUserInfo) 83 | UserGroup.PUT("/password", middleware.JWTAuth(), v1.ChangePassword) 84 | 85 | UserGroup.POST("", v1.CreateUser) 86 | UserGroup.GET("/:id", v1.GetUserById) 87 | UserGroup.PUT("/:id", v1.UpdateUser) 88 | UserGroup.DELETE("/:id", v1.DeleteUser) 89 | } 90 | 91 | // 角色管理 92 | RoleGroup := SystemGroup.Group("/role") 93 | { 94 | RoleGroup.GET("/list", v1.GetAuthorityList) 95 | RoleGroup.GET("/all", v1.GetAllAuthorities) 96 | RoleGroup.POST("", v1.CreateAuthority) 97 | RoleGroup.GET("/:id/menus", v1.GetAuthorityMenus) 98 | RoleGroup.POST("/:id/menus", v1.AssignMenus) 99 | RoleGroup.GET("/:id", v1.GetAuthorityById) 100 | RoleGroup.PUT("/:id", v1.UpdateAuthority) 101 | RoleGroup.DELETE("/:id", v1.DeleteAuthority) 102 | } 103 | 104 | // 菜单管理 105 | MenuGroup := SystemGroup.Group("/menu") 106 | { 107 | MenuGroup.GET("/list", v1.GetMenuList) 108 | MenuGroup.GET("/tree", v1.GetMenuTree) 109 | MenuGroup.POST("", v1.CreateMenu) 110 | MenuGroup.GET("/:id", v1.GetMenuById) 111 | MenuGroup.PUT("/:id", v1.UpdateMenu) 112 | MenuGroup.DELETE("/:id", v1.DeleteMenu) 113 | } 114 | 115 | // 操作日志管理 - 暂时注释掉,需要重新实现 116 | // 操作日志管理路由 117 | OperationLogGroup := SystemGroup.Group("/operation-log") 118 | { 119 | OperationLogGroup.GET("/list", v1.GetOperationLogList) 120 | OperationLogGroup.GET("/stats", v1.GetOperationStats) 121 | OperationLogGroup.GET("/export", v1.ExportOperationLogs) 122 | OperationLogGroup.GET("/:id", v1.GetOperationLogById) 123 | OperationLogGroup.DELETE("/:id", v1.DeleteOperationLog) 124 | OperationLogGroup.DELETE("/batch", v1.DeleteOperationLogsByIds) 125 | OperationLogGroup.DELETE("/clear", v1.ClearOperationLogs) 126 | OperationLogGroup.DELETE("/clear-by-days", v1.ClearOperationLogsByDays) 127 | } 128 | } 129 | fmt.Println("System routes initialized") 130 | 131 | // 暂时移除其他业务路由,后续需要时再添加 132 | // router.InitBusinessRouter(PublicGroup) 133 | } 134 | 135 | fmt.Println(" 欢迎使用 go-gin-element-admin") 136 | fmt.Println(" 当前版本:v1.0.0") 137 | fmt.Println(" 默认自动化文档地址:http://127.0.0.1:8888/swagger/index.html") 138 | fmt.Println(" 默认前端文件运行地址:http://127.0.0.1:8080") 139 | 140 | return Router 141 | } 142 | -------------------------------------------------------------------------------- /web/src/views/test-table-width.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 81 | 82 | 188 | -------------------------------------------------------------------------------- /server/cmd/admin_tool.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "server/core" 7 | "server/global" 8 | "server/initialize" 9 | "server/model/system" 10 | "os" 11 | 12 | "github.com/google/uuid" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | func main() { 17 | var action = flag.String("action", "", "操作类型: reset-admin-permissions, create-admin, show-admin-permissions") 18 | flag.Parse() 19 | 20 | if *action == "" { 21 | fmt.Println("使用方法:") 22 | fmt.Println(" go run cmd/admin_tool.go -action=reset-admin-permissions # 重置管理员权限") 23 | fmt.Println(" go run cmd/admin_tool.go -action=create-admin # 创建管理员用户") 24 | fmt.Println(" go run cmd/admin_tool.go -action=show-admin-permissions # 显示管理员权限") 25 | os.Exit(1) 26 | } 27 | 28 | // 初始化配置和数据库 29 | global.VP = core.Viper() 30 | initialize.OtherInit() 31 | global.DB = initialize.Gorm() 32 | 33 | if global.DB == nil { 34 | fmt.Println("数据库连接失败") 35 | os.Exit(1) 36 | } 37 | 38 | switch *action { 39 | case "reset-admin-permissions": 40 | resetAdminPermissions() 41 | case "create-admin": 42 | createAdminUser() 43 | case "show-admin-permissions": 44 | showAdminPermissions() 45 | default: 46 | fmt.Printf("未知操作: %s\n", *action) 47 | os.Exit(1) 48 | } 49 | } 50 | 51 | // resetAdminPermissions 重置管理员权限 52 | func resetAdminPermissions() { 53 | fmt.Println("开始重置管理员权限...") 54 | initialize.ResetAdminPermissions() 55 | fmt.Println("管理员权限重置完成") 56 | } 57 | 58 | // createAdminUser 创建管理员用户 59 | func createAdminUser() { 60 | db := global.DB 61 | 62 | // 检查管理员角色是否存在 63 | var adminAuth system.SysAuthority 64 | err := db.Where("authority_id = ?", 888).First(&adminAuth).Error 65 | if err == gorm.ErrRecordNotFound { 66 | // 创建管理员角色 67 | adminAuth = system.SysAuthority{ 68 | AuthorityId: 888, 69 | AuthorityName: "管理员", 70 | DefaultRouter: "dashboard", 71 | } 72 | if err := db.Create(&adminAuth).Error; err != nil { 73 | fmt.Printf("创建管理员角色失败: %v\n", err) 74 | return 75 | } 76 | fmt.Println("创建管理员角色成功") 77 | } 78 | 79 | // 检查管理员用户是否存在 80 | var adminUser system.SysUser 81 | if err := db.Where("username = ?", "admin").First(&adminUser).Error; err != nil { 82 | if err == gorm.ErrRecordNotFound { 83 | adminUser = system.SysUser{ 84 | UUID: uuid.New(), 85 | Username: "admin", 86 | NickName: "管理员", 87 | Password: "$2a$10$ve6jMQzd7klAudy.LdDYOOEGMlOqq8zfvuOAut6FuRqAQzjgHh2LG", // 123456 88 | AuthorityId: 888, 89 | HeaderImg: "https://qmplusimg.henrongyi.top/gva_header.jpg", 90 | SideMode: "dark", 91 | Enable: 1, 92 | } 93 | if err := db.Create(&adminUser).Error; err != nil { 94 | fmt.Printf("创建管理员用户失败: %v\n", err) 95 | return 96 | } 97 | fmt.Println("创建管理员用户成功 (用户名: admin, 密码: 123456)") 98 | } else { 99 | fmt.Printf("查询管理员用户失败: %v\n", err) 100 | return 101 | } 102 | } else { 103 | fmt.Println("管理员用户已存在") 104 | } 105 | } 106 | 107 | // showAdminPermissions 显示管理员权限 108 | func showAdminPermissions() { 109 | db := global.DB 110 | 111 | // 获取管理员角色信息 112 | var adminAuth system.SysAuthority 113 | if err := db.Where("authority_id = ?", 888).First(&adminAuth).Error; err != nil { 114 | fmt.Printf("管理员角色不存在: %v\n", err) 115 | return 116 | } 117 | 118 | fmt.Printf("管理员角色信息:\n") 119 | fmt.Printf(" 角色ID: %d\n", adminAuth.AuthorityId) 120 | fmt.Printf(" 角色名称: %s\n", adminAuth.AuthorityName) 121 | fmt.Printf(" 默认路由: %s\n", adminAuth.DefaultRouter) 122 | 123 | // 获取管理员权限 124 | var permissions []struct { 125 | MenuId uint `json:"menuId"` 126 | Title string `json:"title"` 127 | MenuType string `json:"menuType"` 128 | PermissionCode string `json:"permissionCode"` 129 | Path string `json:"path"` 130 | Sort int `json:"sort"` 131 | } 132 | 133 | err := db.Table("sys_authority_menus"). 134 | Select("DISTINCT sys_base_menus.id as menu_id, sys_base_menus.title, sys_base_menus.menu_type, sys_base_menus.permission_code, sys_base_menus.path, sys_base_menus.sort"). 135 | Joins("LEFT JOIN sys_base_menus ON sys_authority_menus.base_menu_id = sys_base_menus.id"). 136 | Where("sys_authority_menus.authority_id = ?", 888). 137 | Order("sys_base_menus.menu_type, sys_base_menus.sort"). 138 | Find(&permissions).Error 139 | 140 | if err != nil { 141 | fmt.Printf("获取管理员权限失败: %v\n", err) 142 | return 143 | } 144 | 145 | fmt.Printf("\n管理员权限列表 (共 %d 个):\n", len(permissions)) 146 | fmt.Println("菜单权限:") 147 | for _, perm := range permissions { 148 | if perm.MenuType == "menu" { 149 | fmt.Printf(" [%d] %s (%s)\n", perm.MenuId, perm.Title, perm.Path) 150 | } 151 | } 152 | 153 | fmt.Println("\n按钮权限:") 154 | for _, perm := range permissions { 155 | if perm.MenuType == "button" { 156 | fmt.Printf(" [%d] %s (%s)\n", perm.MenuId, perm.Title, perm.PermissionCode) 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /server/api/v1/sys_menu.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "server/global" 5 | "server/model/system" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // CreateMenu 创建菜单 13 | func CreateMenu(c *gin.Context) { 14 | var menu system.SysBaseMenu 15 | if err := c.ShouldBindJSON(&menu); err != nil { 16 | c.JSON(http.StatusBadRequest, gin.H{ 17 | "code": 400, 18 | "msg": "参数错误: " + err.Error(), 19 | }) 20 | return 21 | } 22 | 23 | if err := global.DB.Create(&menu).Error; err != nil { 24 | c.JSON(http.StatusInternalServerError, gin.H{ 25 | "code": 500, 26 | "msg": "创建菜单失败: " + err.Error(), 27 | }) 28 | return 29 | } 30 | 31 | c.JSON(http.StatusOK, gin.H{ 32 | "code": 0, 33 | "msg": "创建成功", 34 | "data": menu, 35 | }) 36 | } 37 | 38 | // GetMenuList 获取菜单列表 39 | func GetMenuList(c *gin.Context) { 40 | page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) 41 | pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) 42 | title := c.Query("title") 43 | path := c.Query("path") 44 | 45 | var menus []system.SysBaseMenu 46 | var total int64 47 | 48 | db := global.DB.Model(&system.SysBaseMenu{}) 49 | 50 | // 搜索条件 51 | if title != "" { 52 | db = db.Where("title LIKE ?", "%"+title+"%") 53 | } 54 | if path != "" { 55 | db = db.Where("path LIKE ?", "%"+path+"%") 56 | } 57 | 58 | // 获取总数 59 | db.Count(&total) 60 | 61 | // 分页查询 62 | offset := (page - 1) * pageSize 63 | if err := db.Offset(offset).Limit(pageSize).Order("sort ASC").Find(&menus).Error; err != nil { 64 | c.JSON(http.StatusInternalServerError, gin.H{ 65 | "code": 500, 66 | "msg": "查询失败: " + err.Error(), 67 | }) 68 | return 69 | } 70 | 71 | c.JSON(http.StatusOK, gin.H{ 72 | "code": 0, 73 | "msg": "获取成功", 74 | "data": gin.H{ 75 | "list": menus, 76 | "total": total, 77 | "page": page, 78 | "pageSize": pageSize, 79 | }, 80 | }) 81 | } 82 | 83 | // GetMenuById 根据ID获取菜单 84 | func GetMenuById(c *gin.Context) { 85 | id := c.Param("id") 86 | var menu system.SysBaseMenu 87 | 88 | if err := global.DB.Where("id = ?", id).First(&menu).Error; err != nil { 89 | c.JSON(http.StatusNotFound, gin.H{ 90 | "code": 404, 91 | "msg": "菜单不存在", 92 | }) 93 | return 94 | } 95 | 96 | c.JSON(http.StatusOK, gin.H{ 97 | "code": 0, 98 | "msg": "获取成功", 99 | "data": menu, 100 | }) 101 | } 102 | 103 | // UpdateMenu 更新菜单 104 | func UpdateMenu(c *gin.Context) { 105 | id := c.Param("id") 106 | var menu system.SysBaseMenu 107 | 108 | if err := global.DB.Where("id = ?", id).First(&menu).Error; err != nil { 109 | c.JSON(http.StatusNotFound, gin.H{ 110 | "code": 404, 111 | "msg": "菜单不存在", 112 | }) 113 | return 114 | } 115 | 116 | var updateData system.SysBaseMenu 117 | if err := c.ShouldBindJSON(&updateData); err != nil { 118 | c.JSON(http.StatusBadRequest, gin.H{ 119 | "code": 400, 120 | "msg": "参数错误: " + err.Error(), 121 | }) 122 | return 123 | } 124 | 125 | if err := global.DB.Model(&menu).Updates(updateData).Error; err != nil { 126 | c.JSON(http.StatusInternalServerError, gin.H{ 127 | "code": 500, 128 | "msg": "更新失败: " + err.Error(), 129 | }) 130 | return 131 | } 132 | 133 | c.JSON(http.StatusOK, gin.H{ 134 | "code": 0, 135 | "msg": "更新成功", 136 | }) 137 | } 138 | 139 | // DeleteMenu 删除菜单 140 | func DeleteMenu(c *gin.Context) { 141 | id := c.Param("id") 142 | var menu system.SysBaseMenu 143 | 144 | if err := global.DB.Where("id = ?", id).First(&menu).Error; err != nil { 145 | c.JSON(http.StatusNotFound, gin.H{ 146 | "code": 404, 147 | "msg": "菜单不存在", 148 | }) 149 | return 150 | } 151 | 152 | // 检查是否有子菜单 153 | var childCount int64 154 | global.DB.Model(&system.SysBaseMenu{}).Where("parent_id = ?", id).Count(&childCount) 155 | if childCount > 0 { 156 | c.JSON(http.StatusBadRequest, gin.H{ 157 | "code": 400, 158 | "msg": "该菜单下有子菜单,无法删除", 159 | }) 160 | return 161 | } 162 | 163 | if err := global.DB.Delete(&menu).Error; err != nil { 164 | c.JSON(http.StatusInternalServerError, gin.H{ 165 | "code": 500, 166 | "msg": "删除失败: " + err.Error(), 167 | }) 168 | return 169 | } 170 | 171 | c.JSON(http.StatusOK, gin.H{ 172 | "code": 0, 173 | "msg": "删除成功", 174 | }) 175 | } 176 | 177 | // GetMenuTree 获取菜单树结构 178 | func GetMenuTree(c *gin.Context) { 179 | var menus []system.SysBaseMenu 180 | 181 | if err := global.DB.Order("sort ASC").Find(&menus).Error; err != nil { 182 | c.JSON(http.StatusInternalServerError, gin.H{ 183 | "code": 500, 184 | "msg": "查询失败: " + err.Error(), 185 | }) 186 | return 187 | } 188 | 189 | // 构建树结构 190 | tree := buildMenuTree(menus, nil) 191 | 192 | c.JSON(http.StatusOK, gin.H{ 193 | "code": 0, 194 | "msg": "获取成功", 195 | "data": tree, 196 | }) 197 | } 198 | 199 | // buildMenuTree 构建菜单树结构 200 | func buildMenuTree(menus []system.SysBaseMenu, parentId *uint) []system.SysBaseMenu { 201 | var tree []system.SysBaseMenu 202 | 203 | for _, menu := range menus { 204 | // 比较父ID:都为nil或者值相等 205 | if (menu.ParentId == nil && parentId == nil) || 206 | (menu.ParentId != nil && parentId != nil && *menu.ParentId == *parentId) { 207 | menuId := menu.ID 208 | children := buildMenuTree(menus, &menuId) 209 | menu.Children = children 210 | tree = append(tree, menu) 211 | } 212 | } 213 | 214 | return tree 215 | } 216 | -------------------------------------------------------------------------------- /web/src/views/PermissionTest.vue: -------------------------------------------------------------------------------- 1 | 104 | 105 | 152 | 153 | -------------------------------------------------------------------------------- /web/src/views/system/role/permission.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 160 | 161 | -------------------------------------------------------------------------------- /server/service/system/sys_operation_log.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "server/global" 7 | "server/model/system" 8 | "time" 9 | ) 10 | 11 | type OperationLogService struct{} 12 | 13 | // CreateOperationLog 创建操作日志 14 | func (s *OperationLogService) CreateOperationLog(log *system.SysOperationLog) error { 15 | return global.DB.Create(log).Error 16 | } 17 | 18 | // GetOperationLogList 获取操作日志列表 19 | func (s *OperationLogService) GetOperationLogList(req system.OperationLogRequest) (system.OperationLogResponse, error) { 20 | var logs []system.SysOperationLog 21 | var total int64 22 | 23 | db := global.DB.Model(&system.SysOperationLog{}) 24 | 25 | // 构建查询条件 26 | if req.UserID != 0 { 27 | db = db.Where("user_id = ?", req.UserID) 28 | } 29 | if req.Username != "" { 30 | db = db.Where("username LIKE ?", "%"+req.Username+"%") 31 | } 32 | if req.Method != "" { 33 | db = db.Where("method = ?", req.Method) 34 | } 35 | if req.Path != "" { 36 | db = db.Where("path LIKE ?", "%"+req.Path+"%") 37 | } 38 | if req.OperationType != "" { 39 | db = db.Where("operation_type = ?", req.OperationType) 40 | } 41 | if req.Status != 0 { 42 | db = db.Where("status = ?", req.Status) 43 | } 44 | if req.StartTime != "" { 45 | db = db.Where("operation_time >= ?", req.StartTime) 46 | } 47 | if req.EndTime != "" { 48 | db = db.Where("operation_time <= ?", req.EndTime) 49 | } 50 | 51 | // 获取总数 52 | err := db.Count(&total).Error 53 | if err != nil { 54 | return system.OperationLogResponse{}, err 55 | } 56 | 57 | // 分页查询 58 | if req.Page <= 0 { 59 | req.Page = 1 60 | } 61 | if req.PageSize <= 0 { 62 | req.PageSize = 10 63 | } 64 | 65 | offset := (req.Page - 1) * req.PageSize 66 | err = db.Order("operation_time DESC").Offset(offset).Limit(req.PageSize).Find(&logs).Error 67 | if err != nil { 68 | return system.OperationLogResponse{}, err 69 | } 70 | 71 | return system.OperationLogResponse{ 72 | List: logs, 73 | Total: total, 74 | Page: req.Page, 75 | PageSize: req.PageSize, 76 | }, nil 77 | } 78 | 79 | // DeleteOperationLog 删除操作日志 80 | func (s *OperationLogService) DeleteOperationLog(id uint) error { 81 | return global.DB.Delete(&system.SysOperationLog{}, id).Error 82 | } 83 | 84 | // DeleteOperationLogsByIds 批量删除操作日志 85 | func (s *OperationLogService) DeleteOperationLogsByIds(ids []uint) error { 86 | return global.DB.Delete(&system.SysOperationLog{}, ids).Error 87 | } 88 | 89 | // ClearOperationLogs 清空操作日志 90 | func (s *OperationLogService) ClearOperationLogs() error { 91 | return global.DB.Exec("TRUNCATE TABLE sys_operation_logs").Error 92 | } 93 | 94 | // ClearOperationLogsByDays 清理指定天数前的操作日志 95 | func (s *OperationLogService) ClearOperationLogsByDays(days int) error { 96 | cutoffTime := time.Now().AddDate(0, 0, -days) 97 | return global.DB.Where("operation_time < ?", cutoffTime).Delete(&system.SysOperationLog{}).Error 98 | } 99 | 100 | // GetOperationLogById 根据ID获取操作日志 101 | func (s *OperationLogService) GetOperationLogById(id uint) (system.SysOperationLog, error) { 102 | var log system.SysOperationLog 103 | err := global.DB.Where("id = ?", id).First(&log).Error 104 | return log, err 105 | } 106 | 107 | // LogOperation 记录操作日志的便捷方法 108 | func (s *OperationLogService) LogOperation(userID uint, username, method, path, operationType, description string, requestBody, responseBody interface{}, ip, userAgent string, status int, latency int64, errorMsg string) { 109 | // 序列化请求和响应数据 110 | var reqBodyStr, respBodyStr string 111 | 112 | if requestBody != nil { 113 | if reqBytes, err := json.Marshal(requestBody); err == nil { 114 | reqBodyStr = string(reqBytes) 115 | } 116 | } 117 | 118 | if responseBody != nil { 119 | if respBytes, err := json.Marshal(responseBody); err == nil { 120 | respBodyStr = string(respBytes) 121 | } 122 | } 123 | 124 | // 限制字段长度,避免数据过大 125 | if len(reqBodyStr) > 5000 { 126 | reqBodyStr = reqBodyStr[:5000] + "...[truncated]" 127 | } 128 | if len(respBodyStr) > 5000 { 129 | respBodyStr = respBodyStr[:5000] + "...[truncated]" 130 | } 131 | 132 | log := &system.SysOperationLog{ 133 | UserID: userID, 134 | Username: username, 135 | Method: method, 136 | Path: path, 137 | OperationType: operationType, 138 | Description: description, 139 | RequestBody: reqBodyStr, 140 | ResponseBody: respBodyStr, 141 | IP: ip, 142 | UserAgent: userAgent, 143 | Status: status, 144 | ErrorMessage: errorMsg, 145 | Latency: latency, 146 | OperationTime: time.Now(), 147 | } 148 | 149 | // 异步记录日志,避免影响主业务 150 | go func() { 151 | if err := s.CreateOperationLog(log); err != nil { 152 | fmt.Printf("记录操作日志失败: %v\n", err) 153 | } 154 | }() 155 | } 156 | 157 | // GetOperationStats 获取操作统计信息 158 | func (s *OperationLogService) GetOperationStats() (map[string]interface{}, error) { 159 | var stats map[string]interface{} = make(map[string]interface{}) 160 | 161 | // 今日操作数 162 | today := time.Now().Format("2006-01-02") 163 | var todayCount int64 164 | err := global.DB.Model(&system.SysOperationLog{}). 165 | Where("DATE(operation_time) = ?", today). 166 | Count(&todayCount).Error 167 | if err != nil { 168 | return nil, err 169 | } 170 | stats["todayCount"] = todayCount 171 | 172 | // 总操作数 173 | var totalCount int64 174 | err = global.DB.Model(&system.SysOperationLog{}).Count(&totalCount).Error 175 | if err != nil { 176 | return nil, err 177 | } 178 | stats["totalCount"] = totalCount 179 | 180 | // 操作类型统计 181 | var typeStats []struct { 182 | OperationType string `json:"operationType"` 183 | Count int64 `json:"count"` 184 | } 185 | err = global.DB.Model(&system.SysOperationLog{}). 186 | Select("operation_type, COUNT(*) as count"). 187 | Group("operation_type"). 188 | Find(&typeStats).Error 189 | if err != nil { 190 | return nil, err 191 | } 192 | stats["typeStats"] = typeStats 193 | 194 | // 最近7天操作趋势 195 | var dailyStats []struct { 196 | Date string `json:"date"` 197 | Count int64 `json:"count"` 198 | } 199 | err = global.DB.Model(&system.SysOperationLog{}). 200 | Select("DATE(operation_time) as date, COUNT(*) as count"). 201 | Where("operation_time >= ?", time.Now().AddDate(0, 0, -7)). 202 | Group("DATE(operation_time)"). 203 | Order("date"). 204 | Find(&dailyStats).Error 205 | if err != nil { 206 | return nil, err 207 | } 208 | stats["dailyStats"] = dailyStats 209 | 210 | return stats, nil 211 | } 212 | -------------------------------------------------------------------------------- /web/src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 155 | 156 | -------------------------------------------------------------------------------- /server/middleware/operation_log.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "server/service/system" 8 | "strings" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | // OperationLogMiddleware 操作日志中间件 15 | func OperationLogMiddleware() gin.HandlerFunc { 16 | return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { 17 | // 只记录需要记录的操作 18 | if shouldLogOperation(param.Method, param.Path) { 19 | go recordOperationLog(param) 20 | } 21 | return "" 22 | }) 23 | } 24 | 25 | // ResponseBodyWriter 用于捕获响应体的写入器 26 | type ResponseBodyWriter struct { 27 | gin.ResponseWriter 28 | body *bytes.Buffer 29 | } 30 | 31 | func (r ResponseBodyWriter) Write(b []byte) (int, error) { 32 | r.body.Write(b) 33 | return r.ResponseWriter.Write(b) 34 | } 35 | 36 | // OperationLogMiddlewareWithBody 带请求体和响应体记录的操作日志中间件 37 | func OperationLogMiddlewareWithBody() gin.HandlerFunc { 38 | return func(c *gin.Context) { 39 | // 只对需要记录的操作进行处理 40 | if !shouldLogOperation(c.Request.Method, c.Request.URL.Path) { 41 | c.Next() 42 | return 43 | } 44 | 45 | start := time.Now() 46 | 47 | // 读取请求体 48 | var requestBody []byte 49 | if c.Request.Body != nil { 50 | requestBody, _ = io.ReadAll(c.Request.Body) 51 | c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) 52 | } 53 | 54 | // 创建响应体写入器 55 | responseBodyWriter := &ResponseBodyWriter{ 56 | ResponseWriter: c.Writer, 57 | body: bytes.NewBufferString(""), 58 | } 59 | c.Writer = responseBodyWriter 60 | 61 | // 处理请求 62 | c.Next() 63 | 64 | // 计算耗时 65 | latency := time.Since(start).Milliseconds() 66 | 67 | // 异步记录操作日志 68 | go func() { 69 | recordDetailedOperationLog(c, requestBody, responseBodyWriter.body.Bytes(), latency) 70 | }() 71 | } 72 | } 73 | 74 | // shouldLogOperation 判断是否需要记录操作日志 75 | func shouldLogOperation(method, path string) bool { 76 | // 只记录增删改操作 77 | if method != "POST" && method != "PUT" && method != "DELETE" { 78 | return false 79 | } 80 | 81 | // 排除不需要记录的路径 82 | excludePaths := []string{ 83 | "/api/health", 84 | "/api/system/operation-log", // 避免记录日志查询操作本身 85 | } 86 | 87 | for _, excludePath := range excludePaths { 88 | if strings.HasPrefix(path, excludePath) { 89 | return false 90 | } 91 | } 92 | 93 | // 只记录API路径 94 | return strings.HasPrefix(path, "/api/") 95 | } 96 | 97 | // recordOperationLog 记录操作日志(简单版本) 98 | func recordOperationLog(param gin.LogFormatterParams) { 99 | operationLogService := &system.OperationLogService{} 100 | 101 | // 确定操作类型 102 | operationType := getOperationType(param.Method) 103 | 104 | // 生成操作描述 105 | description := generateDescription(param.Method, param.Path) 106 | 107 | // 获取用户信息(这里需要从上下文中获取,暂时使用默认值) 108 | userID := uint(1) // 默认管理员ID 109 | username := "admin" // 默认用户名 110 | 111 | operationLogService.LogOperation( 112 | userID, 113 | username, 114 | param.Method, 115 | param.Path, 116 | operationType, 117 | description, 118 | nil, // 请求体 119 | nil, // 响应体 120 | param.ClientIP, 121 | "", // UserAgent 122 | param.StatusCode, 123 | param.Latency.Milliseconds(), 124 | param.ErrorMessage, 125 | ) 126 | } 127 | 128 | // recordDetailedOperationLog 记录详细操作日志 129 | func recordDetailedOperationLog(c *gin.Context, requestBody, responseBody []byte, latency int64) { 130 | operationLogService := &system.OperationLogService{} 131 | 132 | // 确定操作类型 133 | operationType := getOperationType(c.Request.Method) 134 | 135 | // 生成操作描述 136 | description := generateDescription(c.Request.Method, c.Request.URL.Path) 137 | 138 | // 获取用户信息(从JWT token或session中获取) 139 | userID, username := getUserInfo(c) 140 | 141 | // 解析请求体和响应体 142 | var reqBodyInterface, respBodyInterface interface{} 143 | 144 | if len(requestBody) > 0 { 145 | json.Unmarshal(requestBody, &reqBodyInterface) 146 | } 147 | 148 | if len(responseBody) > 0 { 149 | json.Unmarshal(responseBody, &respBodyInterface) 150 | } 151 | 152 | // 获取错误信息 153 | errorMessage := "" 154 | if c.Writer.Status() >= 400 { 155 | if respBodyInterface != nil { 156 | if respMap, ok := respBodyInterface.(map[string]interface{}); ok { 157 | if msg, exists := respMap["msg"]; exists { 158 | errorMessage = msg.(string) 159 | } 160 | } 161 | } 162 | } 163 | 164 | operationLogService.LogOperation( 165 | userID, 166 | username, 167 | c.Request.Method, 168 | c.Request.URL.Path, 169 | operationType, 170 | description, 171 | reqBodyInterface, 172 | respBodyInterface, 173 | c.ClientIP(), 174 | c.Request.UserAgent(), 175 | c.Writer.Status(), 176 | latency, 177 | errorMessage, 178 | ) 179 | } 180 | 181 | // getOperationType 根据HTTP方法确定操作类型 182 | func getOperationType(method string) string { 183 | switch method { 184 | case "POST": 185 | return "CREATE" 186 | case "PUT": 187 | return "UPDATE" 188 | case "DELETE": 189 | return "DELETE" 190 | default: 191 | return "OTHER" 192 | } 193 | } 194 | 195 | // generateDescription 生成操作描述 196 | func generateDescription(method, path string) string { 197 | operationType := getOperationType(method) 198 | 199 | // 根据路径生成描述 200 | if strings.Contains(path, "/user") { 201 | switch operationType { 202 | case "CREATE": 203 | return "创建用户" 204 | case "UPDATE": 205 | return "更新用户信息" 206 | case "DELETE": 207 | return "删除用户" 208 | } 209 | } else if strings.Contains(path, "/role") { 210 | switch operationType { 211 | case "CREATE": 212 | return "创建角色" 213 | case "UPDATE": 214 | return "更新角色信息" 215 | case "DELETE": 216 | return "删除角色" 217 | } 218 | } else if strings.Contains(path, "/menu") { 219 | switch operationType { 220 | case "CREATE": 221 | return "创建菜单" 222 | case "UPDATE": 223 | return "更新菜单信息" 224 | case "DELETE": 225 | return "删除菜单" 226 | } 227 | } else if strings.Contains(path, "/authority") { 228 | switch operationType { 229 | case "CREATE": 230 | return "创建权限" 231 | case "UPDATE": 232 | return "更新权限信息" 233 | case "DELETE": 234 | return "删除权限" 235 | } 236 | } else if strings.Contains(path, "/upload") { 237 | return "文件上传" 238 | } else if strings.Contains(path, "/login") { 239 | return "用户登录" 240 | } else if strings.Contains(path, "/logout") { 241 | return "用户登出" 242 | } 243 | 244 | // 根据具体路径生成更详细的描述 245 | switch { 246 | case strings.Contains(path, "/password"): 247 | return "修改密码" 248 | case strings.Contains(path, "/info"): 249 | return "更新个人信息" 250 | case strings.Contains(path, "/batch"): 251 | return "批量" + operationType 252 | case strings.Contains(path, "/assign"): 253 | return "分配权限" 254 | default: 255 | return operationType + " " + extractResourceName(path) 256 | } 257 | } 258 | 259 | // extractResourceName 从路径中提取资源名称 260 | func extractResourceName(path string) string { 261 | parts := strings.Split(path, "/") 262 | if len(parts) >= 3 { 263 | resource := parts[len(parts)-2] 264 | // 转换为中文描述 265 | switch resource { 266 | case "system": 267 | return "系统管理" 268 | case "dashboard": 269 | return "仪表板" 270 | case "operation-log": 271 | return "操作日志" 272 | default: 273 | return resource 274 | } 275 | } 276 | return path 277 | } 278 | 279 | // getUserInfo 从上下文中获取用户信息 280 | func getUserInfo(c *gin.Context) (uint, string) { 281 | // 从JWT token中获取用户信息 282 | if userClaims, exists := c.Get("claims"); exists { 283 | if claims, ok := userClaims.(map[string]interface{}); ok { 284 | var userID uint 285 | var username string 286 | 287 | // 获取用户ID 288 | if id, exists := claims["userID"]; exists { 289 | if idFloat, ok := id.(float64); ok { 290 | userID = uint(idFloat) 291 | } 292 | } 293 | 294 | // 获取用户名 295 | if name, exists := claims["username"]; exists { 296 | if nameStr, ok := name.(string); ok { 297 | username = nameStr 298 | } 299 | } 300 | 301 | // 如果获取到了有效信息,返回 302 | if userID > 0 && username != "" { 303 | return userID, username 304 | } 305 | } 306 | } 307 | 308 | // 尝试从其他方式获取用户信息 309 | if userID, exists := c.Get("userID"); exists { 310 | if username, exists := c.Get("username"); exists { 311 | if id, ok := userID.(uint); ok { 312 | if name, ok := username.(string); ok { 313 | return id, name 314 | } 315 | } 316 | } 317 | } 318 | 319 | // 默认返回系统用户信息 320 | return 0, "system" 321 | } 322 | -------------------------------------------------------------------------------- /server/api/v1/sys_operation_log.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | system2 "server/model/system" 7 | "server/service/system" 8 | "strconv" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | var operationLogService = system.OperationLogService{} 14 | 15 | // GetOperationLogList 获取操作日志列表 16 | // @Tags 操作日志 17 | // @Summary 获取操作日志列表 18 | // @Security ApiKeyAuth 19 | // @accept application/json 20 | // @Produce application/json 21 | // @Param data query system.OperationLogRequest true "查询参数" 22 | // @Success 200 {object} response.Response{data=system.OperationLogResponse,msg=string} "获取成功" 23 | // @Router /system/operation-log/list [get] 24 | func GetOperationLogList(c *gin.Context) { 25 | var req system2.OperationLogRequest 26 | err := c.ShouldBindQuery(&req) 27 | if err != nil { 28 | c.JSON(http.StatusBadRequest, gin.H{ 29 | "code": 400, 30 | "msg": "参数错误: " + err.Error(), 31 | }) 32 | return 33 | } 34 | 35 | list, err := operationLogService.GetOperationLogList(req) 36 | if err != nil { 37 | c.JSON(http.StatusInternalServerError, gin.H{ 38 | "code": 500, 39 | "msg": "获取失败: " + err.Error(), 40 | }) 41 | return 42 | } 43 | 44 | c.JSON(http.StatusOK, gin.H{ 45 | "code": 0, 46 | "msg": "获取成功", 47 | "data": gin.H{ 48 | "list": list.List, 49 | "total": list.Total, 50 | "page": list.Page, 51 | "pageSize": list.PageSize, 52 | }, 53 | }) 54 | } 55 | 56 | // GetOperationLogById 根据ID获取操作日志详情 57 | // @Tags 操作日志 58 | // @Summary 根据ID获取操作日志详情 59 | // @Security ApiKeyAuth 60 | // @accept application/json 61 | // @Produce application/json 62 | // @Param id path int true "操作日志ID" 63 | // @Success 200 {object} response.Response{data=system.SysOperationLog,msg=string} "获取成功" 64 | // @Router /system/operation-log/{id} [get] 65 | func GetOperationLogById(c *gin.Context) { 66 | idStr := c.Param("id") 67 | id, err := strconv.ParseUint(idStr, 10, 32) 68 | if err != nil { 69 | c.JSON(http.StatusBadRequest, gin.H{ 70 | "code": 400, 71 | "msg": "ID格式错误", 72 | }) 73 | return 74 | } 75 | 76 | log, err := operationLogService.GetOperationLogById(uint(id)) 77 | if err != nil { 78 | c.JSON(http.StatusNotFound, gin.H{ 79 | "code": 404, 80 | "msg": "操作日志不存在", 81 | }) 82 | return 83 | } 84 | 85 | c.JSON(http.StatusOK, gin.H{ 86 | "code": 0, 87 | "msg": "获取成功", 88 | "data": log, 89 | }) 90 | } 91 | 92 | // DeleteOperationLog 删除操作日志 93 | // @Tags 操作日志 94 | // @Summary 删除操作日志 95 | // @Security ApiKeyAuth 96 | // @accept application/json 97 | // @Produce application/json 98 | // @Param id path int true "操作日志ID" 99 | // @Success 200 {object} response.Response{msg=string} "删除成功" 100 | // @Router /system/operation-log/{id} [delete] 101 | func DeleteOperationLog(c *gin.Context) { 102 | idStr := c.Param("id") 103 | id, err := strconv.ParseUint(idStr, 10, 32) 104 | if err != nil { 105 | c.JSON(http.StatusBadRequest, gin.H{ 106 | "code": 400, 107 | "msg": "ID格式错误", 108 | }) 109 | return 110 | } 111 | 112 | err = operationLogService.DeleteOperationLog(uint(id)) 113 | if err != nil { 114 | c.JSON(http.StatusInternalServerError, gin.H{ 115 | "code": 500, 116 | "msg": "删除失败: " + err.Error(), 117 | }) 118 | return 119 | } 120 | 121 | c.JSON(http.StatusOK, gin.H{ 122 | "code": 0, 123 | "msg": "删除成功", 124 | }) 125 | } 126 | 127 | // DeleteOperationLogsByIds 批量删除操作日志 128 | // @Tags 操作日志 129 | // @Summary 批量删除操作日志 130 | // @Security ApiKeyAuth 131 | // @accept application/json 132 | // @Produce application/json 133 | // @Param data body request.IdsReq true "操作日志ID列表" 134 | // @Success 200 {object} response.Response{msg=string} "删除成功" 135 | // @Router /system/operation-log/batch [delete] 136 | func DeleteOperationLogsByIds(c *gin.Context) { 137 | var req struct { 138 | Ids []uint `json:"ids" binding:"required"` 139 | } 140 | err := c.ShouldBindJSON(&req) 141 | if err != nil { 142 | c.JSON(http.StatusBadRequest, gin.H{ 143 | "code": 400, 144 | "msg": "参数错误: " + err.Error(), 145 | }) 146 | return 147 | } 148 | 149 | err = operationLogService.DeleteOperationLogsByIds(req.Ids) 150 | if err != nil { 151 | c.JSON(http.StatusInternalServerError, gin.H{ 152 | "code": 500, 153 | "msg": "删除失败: " + err.Error(), 154 | }) 155 | return 156 | } 157 | 158 | c.JSON(http.StatusOK, gin.H{ 159 | "code": 0, 160 | "msg": "删除成功", 161 | }) 162 | } 163 | 164 | // ClearOperationLogs 清空所有操作日志 165 | // @Tags 操作日志 166 | // @Summary 清空所有操作日志 167 | // @Security ApiKeyAuth 168 | // @accept application/json 169 | // @Produce application/json 170 | // @Success 200 {object} response.Response{msg=string} "清空成功" 171 | // @Router /system/operation-log/clear [delete] 172 | func ClearOperationLogs(c *gin.Context) { 173 | err := operationLogService.ClearOperationLogs() 174 | if err != nil { 175 | c.JSON(http.StatusInternalServerError, gin.H{ 176 | "code": 500, 177 | "msg": "清空失败: " + err.Error(), 178 | }) 179 | return 180 | } 181 | 182 | c.JSON(http.StatusOK, gin.H{ 183 | "code": 0, 184 | "msg": "清空成功", 185 | }) 186 | } 187 | 188 | // ClearOperationLogsByDays 清理指定天数前的操作日志 189 | // @Tags 操作日志 190 | // @Summary 清理指定天数前的操作日志 191 | // @Security ApiKeyAuth 192 | // @accept application/json 193 | // @Produce application/json 194 | // @Param data body request.DaysReq true "保留天数" 195 | // @Success 200 {object} response.Response{msg=string} "清理成功" 196 | // @Router /system/operation-log/clear-by-days [delete] 197 | func ClearOperationLogsByDays(c *gin.Context) { 198 | var req struct { 199 | Days int `json:"days" binding:"required,min=1"` 200 | } 201 | err := c.ShouldBindJSON(&req) 202 | if err != nil { 203 | c.JSON(http.StatusBadRequest, gin.H{ 204 | "code": 400, 205 | "msg": "参数错误: " + err.Error(), 206 | }) 207 | return 208 | } 209 | 210 | err = operationLogService.ClearOperationLogsByDays(req.Days) 211 | if err != nil { 212 | c.JSON(http.StatusInternalServerError, gin.H{ 213 | "code": 500, 214 | "msg": "清理失败: " + err.Error(), 215 | }) 216 | return 217 | } 218 | 219 | c.JSON(http.StatusOK, gin.H{ 220 | "code": 0, 221 | "msg": "清理成功", 222 | }) 223 | } 224 | 225 | // GetOperationStats 获取操作统计信息 226 | // @Tags 操作日志 227 | // @Summary 获取操作统计信息 228 | // @Security ApiKeyAuth 229 | // @accept application/json 230 | // @Produce application/json 231 | // @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功" 232 | // @Router /system/operation-log/stats [get] 233 | func GetOperationStats(c *gin.Context) { 234 | stats, err := operationLogService.GetOperationStats() 235 | if err != nil { 236 | c.JSON(http.StatusInternalServerError, gin.H{ 237 | "code": 500, 238 | "msg": "获取失败: " + err.Error(), 239 | }) 240 | return 241 | } 242 | 243 | c.JSON(http.StatusOK, gin.H{ 244 | "code": 0, 245 | "msg": "获取成功", 246 | "data": stats, 247 | }) 248 | } 249 | 250 | // ExportOperationLogs 导出操作日志 251 | // @Tags 操作日志 252 | // @Summary 导出操作日志 253 | // @Security ApiKeyAuth 254 | // @accept application/json 255 | // @Produce application/json 256 | // @Param data query system.OperationLogRequest true "查询参数" 257 | // @Success 200 {object} response.Response{msg=string} "导出成功" 258 | // @Router /system/operation-log/export [get] 259 | func ExportOperationLogs(c *gin.Context) { 260 | var req system2.OperationLogRequest 261 | err := c.ShouldBindQuery(&req) 262 | if err != nil { 263 | c.JSON(http.StatusBadRequest, gin.H{ 264 | "code": 400, 265 | "msg": "参数错误: " + err.Error(), 266 | }) 267 | return 268 | } 269 | 270 | // 设置大的页面大小以获取所有数据 271 | req.PageSize = 10000 272 | req.Page = 1 273 | 274 | list, err := operationLogService.GetOperationLogList(req) 275 | if err != nil { 276 | c.JSON(http.StatusInternalServerError, gin.H{ 277 | "code": 500, 278 | "msg": "导出失败: " + err.Error(), 279 | }) 280 | return 281 | } 282 | 283 | // 设置响应头为CSV文件 284 | c.Header("Content-Type", "text/csv") 285 | c.Header("Content-Disposition", "attachment; filename=operation_logs.csv") 286 | 287 | // 写入CSV头部 288 | c.Writer.WriteString("\xEF\xBB\xBF") // UTF-8 BOM 289 | c.Writer.WriteString("ID,用户名,操作类型,请求方法,请求路径,操作描述,状态码,IP地址,耗时(ms),操作时间,错误信息\n") 290 | 291 | // 写入数据 292 | for _, log := range list.List { 293 | c.Writer.WriteString(fmt.Sprintf("%d,%s,%s,%s,%s,%s,%d,%s,%d,%s,%s\n", 294 | log.ID, 295 | log.Username, 296 | log.OperationType, 297 | log.Method, 298 | log.Path, 299 | log.Description, 300 | log.Status, 301 | log.IP, 302 | log.Latency, 303 | log.OperationTime.Format("2006-01-02 15:04:05"), 304 | log.ErrorMessage, 305 | )) 306 | } 307 | 308 | c.Status(http.StatusOK) 309 | } 310 | -------------------------------------------------------------------------------- /web/src/views/PermissionDemo.vue: -------------------------------------------------------------------------------- 1 | 164 | 165 | 229 | 230 | -------------------------------------------------------------------------------- /web/src/views/system/user/index.vue: -------------------------------------------------------------------------------- 1 | 127 | 128 | 283 | 284 | -------------------------------------------------------------------------------- /web/src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 119 | 120 | -------------------------------------------------------------------------------- /web/src/components/IconSelector.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 336 | 337 | -------------------------------------------------------------------------------- /web/src/views/system/menu/index.vue: -------------------------------------------------------------------------------- 1 | 159 | 160 | 323 | 324 | -------------------------------------------------------------------------------- /web/src/views/profile/index.vue: -------------------------------------------------------------------------------- 1 | 127 | 128 | 346 | 347 | --------------------------------------------------------------------------------