├── 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 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
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 |
2 |
3 |
4 |
404
5 |
页面不存在
6 |
7 | 抱歉,您访问的页面不存在或已被删除
8 |
9 |
10 |
11 | 返回首页
12 |
13 |
14 | 返回上页
15 |
16 |
17 |
18 |
19 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
6 |
7 |
28 |
29 |
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 |
2 |
40 |
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 |
2 |
3 |
表格宽度测试
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 | 测试表格 - 应该与容器宽度一致
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | 编辑
31 | 删除
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
宽度设置说明:
41 |
42 | - 容器设置:width: 100%
43 | - 表格设置:width: 100% !important
44 | - 表格容器:width: 100%, overflow-x: auto
45 | - 响应式:大屏幕完全展开,小屏幕允许横向滚动
46 |
47 |
48 |
49 |
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 |
2 |
3 |
4 |
5 | 权限调试测试页面
6 |
7 |
8 |
9 |
10 |
11 | {{ userInfo.id || '未获取' }}
12 | {{ userInfo.username || '未获取' }}
13 | {{ userInfo.nickName || '未获取' }}
14 | {{ userInfo.authority?.authorityId || '未获取' }}
15 | {{ userInfo.authority?.authorityName || '未获取' }}
16 |
17 |
18 |
19 |
20 |
权限列表
21 |
22 |
28 | {{ permission }}
29 |
30 |
31 | 暂无权限
32 |
33 |
34 |
权限总数: {{ userPermissions.length }}
35 |
36 |
37 |
38 |
39 |
用户菜单
40 |
41 |
42 |
43 |
44 |
45 |
权限测试
46 |
47 |
52 | 新增用户按钮 (user:create)
53 |
54 |
55 |
60 | 编辑用户按钮 (user:update)
61 |
62 |
63 |
68 | 删除用户按钮 (user:delete)
69 |
70 |
71 |
76 | 新增角色按钮 (role:create)
77 |
78 |
79 |
80 |
81 |
82 |
83 |
手动权限测试
84 |
89 |
测试权限
90 |
91 | 测试结果: {{ testResult ? '有权限' : '无权限' }}
92 |
93 |
94 |
95 |
96 |
97 | 刷新数据
98 | 清空控制台
99 |
100 |
101 |
102 |
103 |
104 |
105 |
152 |
153 |
--------------------------------------------------------------------------------
/web/src/views/system/role/permission.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
16 |
17 |
18 |
19 |
20 |
21 | {{ roleInfo.authorityId }}
22 | {{ roleInfo.authorityName }}
23 | {{ roleInfo.defaultRouter }}
24 | {{ formatDate(roleInfo.createdAt) }}
25 |
26 |
27 |
28 |
52 |
53 |
54 |
55 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
{{ stats.userCount }}
12 |
用户总数
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
{{ stats.roleCount }}
26 |
角色总数
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
{{ stats.menuCount }}
40 |
菜单总数
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
54 |
55 |
56 |
57 | 系统版本:
58 | {{ systemInfo.systemVersion }}
59 |
60 |
61 | Go版本:
62 | {{ systemInfo.goVersion }}
63 |
64 |
65 | Gin版本:
66 | {{ systemInfo.ginVersion }}
67 |
68 |
69 | Vue版本:
70 | {{ systemInfo.vueVersion }}
71 |
72 |
73 | Element Plus:
74 | {{ systemInfo.elementPlus }}
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
86 |
87 |
88 |
89 | 20250610
90 | 发布初版
91 |
92 |
93 |
94 |
95 |
96 |
97 |
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 |
2 |
3 |
4 |
5 | 按钮权限控制演示
6 |
7 |
8 |
9 |
15 | 本系统实现了基于权限编码的按钮级权限控制,支持以下功能:
16 |
17 | - 权限指令:使用
v-permission="'权限编码'" 控制按钮显示
18 | - 权限函数:使用
hasPermission('权限编码') 进行权限判断
19 | - 角色指令:使用
v-role="'角色名'" 控制基于角色的显示
20 | - 支持多权限组合判断
21 |
22 |
23 |
24 |
25 |
当前用户权限
26 |
27 |
33 | {{ permission }}
34 |
35 |
36 | 暂无权限编码
37 |
38 |
39 |
40 |
41 |
42 |
权限指令演示
43 |
44 |
单个权限判断
45 |
46 |
47 | 用户创建 (user:create)
48 |
49 |
50 | 用户编辑 (user:update)
51 |
52 |
53 | 用户删除 (user:delete)
54 |
55 |
56 | 超管权限 (admin:all)
57 |
58 |
59 |
60 |
61 |
62 |
多权限组合判断
63 |
64 |
65 | 用户创建或编辑
66 |
67 |
68 | 角色创建或编辑
69 |
70 |
71 |
72 |
73 |
74 |
75 |
编程式权限判断
76 |
77 |
// 使用权限函数进行判断
78 | if (hasPermission('user:create')) {
79 | // 有权限时的逻辑
80 | console.log('可以创建用户')
81 | } else {
82 | // 无权限时的逻辑
83 | console.log('无创建用户权限')
84 | }
85 |
86 | // 多权限判断 - 任一权限
87 | hasPermission(['user:create', 'user:update'], 'some')
88 |
89 | // 多权限判断 - 全部权限
90 | hasPermission(['user:create', 'user:update'], 'every')
91 |
92 |
93 |
94 |
95 | 测试用户创建权限
96 |
97 |
98 | 测试用户删除权限
99 |
100 |
101 | 测试多权限组合
102 |
103 |
104 |
105 |
106 |
107 |
菜单权限配置示例
108 |
109 |
110 |
111 |
112 |
113 | {{ row.menuType === 'button' ? '按钮' : '菜单' }}
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | {{ row.permissionCode }}
122 |
123 | -
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
最佳实践
132 |
138 |
139 |
命名规范:
140 |
141 | - 模块:操作 格式,如
user:create
142 | - 资源:动作 格式,如
order:export
143 | - 使用小写字母和冒号分隔
144 | - 保持简洁和语义化
145 |
146 |
147 |
常用权限编码示例:
148 |
149 | user:create
150 | user:update
151 | user:delete
152 | user:view
153 | role:assign
154 | order:export
155 | system:backup
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
229 |
230 |
--------------------------------------------------------------------------------
/web/src/views/system/user/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 搜索
27 |
28 |
29 |
30 | 重置
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {{ formatDate(row.CreatedAt) }}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
78 |
79 |
80 |
81 |
86 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
114 |
115 |
116 |
117 |
118 |
119 | 取消
120 |
121 | 确定
122 |
123 |
124 |
125 |
126 |
127 |
128 |
283 |
284 |
--------------------------------------------------------------------------------
/web/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
65 |
66 |
67 |
119 |
120 |
--------------------------------------------------------------------------------
/web/src/components/IconSelector.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
55 |
56 |
57 |
58 | {{ icon.name }}
59 |
60 |
61 |
62 |
63 | 清除选择
64 |
65 |
66 |
67 |
68 |
69 |
70 |
336 |
337 |
--------------------------------------------------------------------------------
/web/src/views/system/menu/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
158 |
159 |
160 |
323 |
324 |
--------------------------------------------------------------------------------
/web/src/views/profile/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
{{ userInfo.nickName || userInfo.username }}
16 |
{{ userInfo.email }}
17 |
{{ userInfo.authority?.authorityName || '普通用户' }}
18 |
19 |
20 |
21 |
22 |
23 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
55 |
56 |
![]()
57 |
58 |
59 |
60 |
61 |
支持 JPG、PNG、GIF 格式
62 |
文件大小不超过 10MB
63 |
点击头像上传新图片
64 |
65 |
66 |
67 |
68 |
69 |
70 | 更新信息
71 |
72 | 重置
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
84 |
85 |
86 |
93 |
94 |
99 |
100 |
101 |
102 |
107 |
108 |
109 |
110 |
115 |
116 |
117 |
118 |
119 | 修改密码
120 |
121 | 重置
122 |
123 |
124 |
125 |
126 |
127 |
128 |
346 |
347 |
--------------------------------------------------------------------------------