1 项目说明
1.1 简介
01.常用信息
a.介绍
Gin-Vue-Admin 是一个全栈管理框架,专为快速开发Web应用程序而设计,具有完整的前后端分离架构。
基于Go (Gin) 和Vue.js构建,提供了一个全面的开发平台,具备自动化代码生成、AI辅助开发和企业级安全特性。
b.线上部署
GitHub地址: https://github.com/flipped-aurora/gin-vue-admin
GitCode地址: https://gitCode.com/flipped-aurora/gin-vue-admin
Gitee地址: https://gitee.com/pixelmax/gin-vue-admin
在线演示: http://demo.gin-vue-admin.com/
用户名:admin
密码:123456
c.项目定位
Gin-Vue-Admin 作为企业级管理系统的基础框架,专注于为开发者提供:
🚀 快速开发: AutoCode生成系统,可在1分钟内生成完整的CRUD功能
🔒 企业安全: JWT认证 + Casbin RBAC授权的双重安全保障
🔧 高度灵活: 动态路由、菜单管理和API配置
📚 完整文档: Swagger API文档自动生成
☁️ 云原生: 多云文件存储支持(七牛云、阿里云、AWS S3)
🗄️ 多数据库: 支持MySQL、PostgreSQL、SQLite和MSSQL
该系统主要面向构建管理后台、内容管理系统和需要用户管理及权限控制的业务应用的开发者。
02.常用信息
a.环境要求
Node.js: ≥ 18.16.2
Go: ≥ 1.22
MySQL: ≥ 5.7 (引擎必须为 InnoDB)
Git: 版本控制工具
推荐使用 Docker 创建 MySQL 数据库以确保环境一致性
b.前端技术栈
技术 版本 描述
Vue.js 3.3.4 渐进式JavaScript框架
Element Plus 2.3.8 Vue 3 UI组件库
Pinia Latest 状态管理(替代Vuex)
Vue Router Latest SPA路由与动态路由
Vite Latest 构建工具和开发服务器
c.后端技术栈
技术 版本 描述
Go ≥ 1.22 编程语言
Gin 1.9.1 高性能Web框架
GORM 1.25.2 ORM库,支持自动迁移
Casbin Latest 访问控制库(RBAC)
d.数据库支持
数据库 版本要求 说明
MySQL ≥ 5.7 主数据库,InnoDB引擎
PostgreSQL ≥ 9.6 关系型数据库替代方案
SQLite Latest 嵌入式数据库选项
MS SQL Server Latest 微软数据库支持
Oracle Latest 企业级数据库支持
e.缓存与存储
Redis: 缓存和会话管理
七牛云: 对象存储服务
阿里云OSS: 阿里巴巴云存储
AWS S3: 亚马逊Web服务存储
f.开发工具
Swagger: API文档自动生成
Viper: 配置管理
Zap: 结构化日志记录
fsnotify: 文件系统通知
g.AI集成
LLM APIs: AI辅助代码生成
MCP Protocol: 模型上下文协议,用于AI代理
03.核心功能
a.安全认证系统
JWT认证: 无状态的用户身份验证
Casbin RBAC: 基于角色的访问控制
多点登录控制: 支持单点登录限制
API权限管理: 细粒度的接口访问控制
b.用户权限管理
用户管理: 系统管理员分配用户角色和权限
角色管理: 创建权限控制对象,支持API、菜单、按钮权限分配
菜单管理: 动态菜单配置,实现不同角色不同菜单
按钮权限: 页面级别的操作权限控制
c.快速开发工具
AutoCode生成器: 1分钟生成完整CRUD功能的代码生成器
表单生成器: 基于 Variant Form 的可视化表单设计
API自动文档: Swagger自动生成API文档
RESTful示例: 标准的RESTful API设计参考
d.文件存储系统
多云存储: 支持本地、七牛云、阿里云、腾讯云存储
分片上传: 大文件分片上传功能
断点续传: 文件上传中断后可继续上传
文件管理: 完整的文件上传下载管理
e.系统管理
配置管理: 前台可视化配置文件修改
日志管理: 系统操作日志记录和查询
监控面板: 系统运行状态监控
数据字典: 系统数据字典管理
f.用户界面
富文本编辑器: 内置MarkDown编辑器
条件搜索: 高级搜索功能示例
数据导入导出: Excel数据处理功能
响应式设计: 适配多种设备屏幕
g.插件生态
插件中心 NEW: 基于GVA设计的Go插件中心
微信集成: 微信支付、登录等功能插件
K8s操作: Kubernetes相关操作插件
第三方登录: 多种第三方登录方式支持
04.系统设计
a.图示
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 前端层 (Vue) │ │ 后端层 (Go) │ │ 数据层 (DB) │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ • Vue 3 + Vite │ │ • Gin Framework │ │ • MySQL/PG/... │
│ • Element Plus │◄──►│ • JWT + Casbin │◄──►│ • Redis Cache │
│ • Pinia Store │ │ • GORM ORM │ │ • File Storage │
│ • Vue Router │ │ • Swagger Docs │ │ • Cloud Storage │
└─────────────────┘ └─────────────────┘ └─────────────────┘
b.前端架构 (Vue.js + Vite)
应用启动: main.js - 应用程序引导
路由系统: Vue Router - 动态路由管理
状态管理: Pinia Stores - 全局状态管理
布局系统: Header, Aside, Tabs - 页面布局组件
权限控制: 菜单权限、API权限、按钮权限
组件库: Upload, Select, Export - 通用业务组件
c.后端架构 (Go + Gin)
服务启动: main.go - 服务器引导程序
配置系统: core.Viper - 配置管理系统
认证中间件: middleware.JWT - JWT身份验证
授权中间件: middleware.Casbin - 权限控制处理
API处理器: api.v1 - REST API处理程序
业务服务: service.* - 业务逻辑服务
代码生成: service.AutoCode - 自动代码生成引擎
d.数据架构
主数据库: global.GVA_DB - 主要数据存储
缓存系统: global.GVA_REDIS - 缓存和会话
文件存储: 本地/云端对象存储服务
权限存储: system.CasbinRule - 策略存储
e.认证授权流程
sequenceDiagram
participant U as 用户
participant F as 前端
participant A as API网关
participant S as 业务服务
participant D as 数据库
U->>F: 登录请求
F->>A: POST /base/login
A->>S: 验证用户凭据
S->>D: 查询用户信息
D-->>S: 返回用户数据
S->>A: 生成JWT Token
A-->>F: 返回Token
F->>F: 存储Token
U->>F: 访问受保护资源
F->>A: 请求 + JWT Header
A->>A: JWT验证
A->>S: Casbin权限检查
S-->>A: 权限结果
A-->>F: 返回数据/拒绝访问
f.初始化流程
配置初始化: core.Viper - 加载配置文件
日志初始化: core.Zap - 设置日志系统
数据库连接: initialize.Gorm - 建立数据库连接
数据表注册: initialize.RegisterTables - 注册数据模型
Redis连接: initialize.Redis - 建立缓存连接
路由初始化: initialize.Routers - 设置API路由
服务启动: core.RunWindowsServer - 启动HTTP服务
1.2 环境配置
01.环境准备
a.环境要求
Git: 用于代码版本管理。
Node.js: >= 18.16.2 (推荐使用 LTS 版本)。
Go: >= 1.22 (推荐使用最新稳定版)。
MySQL: >= 8.0,存储引擎必须为 InnoDB。
Redis: >= 6.0 (可选,用于缓存)。
b.分支状态说明
main:🟢 活跃维护,主分支,生产环境推荐。
i18n-dev-new:🟡 更新中,组合式API多语言版本。
v2.4.x:🔴 停止维护,声明式API版本。
i18n-dev:🔴 停止维护,声明式API多语言版本。
c.默认管理员账号
用户名:admin
密码:123456
d.2.4.5版本之前
闭源项目:无任何限制
开源项目:需保留原始协议声明
商业使用:无需额外授权
e.2.4.5版本及以后
个人学习项目:完全免费
企业内部系统(不对外开放,不接入公网):完全免费
外包项目/对外开放系统:必须获得商业授权
商业盈利项目:必须获得商业授权
02.Node.js 环境安装
a.下载安装 Node.js
访问 Node.js 官网下载并安装 LTS 版本。
推荐版本: Node.js 18.x 或更高版本。
b.验证安装
# 检查 Node.js 版本
node -v
# 检查 npm 版本
npm -v
c.配置 npm 镜像源(可选)
# 设置淘宝镜像源
npm config set registry https://registry.npmmirror.com
# 验证配置
npm config get registry
03.Go 环境安装
a.下载安装 Go
根据您的网络环境选择下载地址:
国际用户: https://golang.org/dl/
国内用户: https://golang.google.cn/dl/
推荐版本: Go 1.22 或更高版本。
b.验证安装
# 检查 Go 版本
go version
# 查看 Go 环境信息
go env
c.配置 Go 模块代理(推荐)
# 启用 Go Modules
go env -w GO111MODULE=on
# 配置模块代理
go env -w GOPROXY=https://goproxy.cn,direct
# 配置私有模块跳过代理
go env -w GOPRIVATE=*.corp.example.com
04.数据库环境
a.MySQL 安装
a.macOS
# 使用 Homebrew 安装
brew install mysql
# 启动 MySQL 服务
brew services start mysql
b.Ubuntu/Debian
# 更新包列表
sudo apt update
# 安装 MySQL
sudo apt install mysql-server
# 启动 MySQL 服务
sudo systemctl start mysql
sudo systemctl enable mysql
c.Windows
访问 MySQL 官网下载安装包。
b.Redis 安装(可选)
a.macOS
brew install redis
brew services start redis
b.Ubuntu/Debian
sudo apt install redis-server
sudo systemctl start redis-server
sudo systemctl enable redis-server
05.开发工具配置
a.VS Code 推荐插件
{
"recommendations": [
"golang.go",
"vue.volar",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"ms-vscode.vscode-typescript-next",
"formulahendry.auto-rename-tag",
"christian-kohler.path-intellisense"
]
}
b.GoLand 配置建议
Go Modules: 确保启用 Go Modules 支持。
代码格式化: 配置 gofmt 和 goimports。
代码检查: 启用 golint 和 go vet。
06.环境验证
a.完成环境安装后,请运行以下命令验证环境是否正确配置:
# 检查 Git
git --version
# 检查 Node.js 和 npm
node -v && npm -v
# 检查 Go
go version
# 检查 MySQL(需要先启动服务)
mysql --version
# 检查 Redis(如果安装了)
redis-cli --version
b.验证结果
如果所有命令都能正常输出版本信息,说明环境配置成功!
1.3 项目初始化
01.快速启动
a.克隆项目
# 克隆主分支代码
git clone https://github.com/flipped-aurora/gin-vue-admin.git
# 进入项目目录
cd gin-vue-admin
b.项目结构概览
gin-vue-admin/
├── server/ # 后端 Go 项目
│ ├── main.go # 程序入口
│ ├── config.yaml # 配置文件
│ └── ...
├── web/ # 前端 Vue 项目
│ ├── src/ # 源代码
│ ├── package.json # 依赖配置
│ └── ...
└── README.md # 项目说明
02.后端服务启动
a.打开后端项目
# 进入后端目录
cd server
# 使用 GoLand 打开(如果已安装)
goland .
# 或使用 VS Code 打开
code .
b.安装依赖
# 添加缺失的模块依赖,移除未使用的依赖
# 该命令会根据 go.mod 文件下载所需的第三方包
go mod tidy
c.启动后端服务
方式一:命令行启动
# 在 server 目录下运行
go run main.go
-----------------------------------------------------------------------------------------------------
方式二:GoLand 启动
在 GoLand 中打开 main.go 文件
点击行号旁的绿色三角形按钮
或使用快捷键 Ctrl+Shift+F10 (Windows/Linux) 或 Cmd+Shift+R (macOS)
-----------------------------------------------------------------------------------------------------
方式三:VS Code 启动
按 F5 或点击调试按钮
选择 "Go: Launch Package"
d.验证后端启动
如果看到以下输出,说明后端服务启动成功:
[GIN-debug] Listening and serving HTTP on :8888
访问 http://localhost:8888/health 检查服务状态。
03.前端应用启动
a.打开前端项目
# 进入前端目录
cd web
# 使用 VS Code 打开
code .
b.安装依赖
# 安装项目依赖
npm install
# 或使用 yarn(如果已安装)
yarn install
# 或使用 pnpm(推荐,速度更快)
pnpm install
c.启动开发服务器
# 启动开发服务器
npm run serve
# 或使用其他包管理器
yarn serve
pnpm serve
d.验证前端启动
如果看到以下输出,说明前端应用启动成功:
App running at:
- Local: http://localhost:8080/
- Network: http://192.168.1.100:8080/
04.数据库初始化
a.访问初始化页面
在浏览器中访问:http://localhost:8080/#/init
b.配置数据库信息
在初始化页面填写数据库连接信息:
数据库类型: 选择 MySQL
主机地址: 127.0.0.1
端口: 3306
用户名: root
密码: 您的数据库密码
数据库名: gva(如果不存在会自动创建)
c.执行初始化
确认所有信息无误
点击 "立即初始化" 按钮
等待初始化完成
d.初始化完成
初始化成功后,系统会:
✅ 创建所有必要的数据表
✅ 插入基础数据(管理员账号、菜单、权限等)
✅ 自动跳转到登录页面
e.默认管理员账号
用户名:admin
密码:123456
1.4 AI助手集成
01.MCP AI助手集成
a.功能说明
通过MCP(Model Context Protocol)让AI编辑工具深度理解GVA项目结构,实现智能化的代码生成和项目管理。
b.版本要求
使用MCP功能需要GVA版本 ≥ 2.8.4。
推荐使用模型:claude > gemini > gpt = kimi。
c.核心特性
智能代码生成:AI自动创建完整的CRUD模板。
智能文件搜索:自动定位相关文件并提供精准修改建议。
自动化流程:一键生成API接口和菜单配置。
上下文理解:AI深度理解项目架构,提供更准确的代码联动。
02.AI编辑工具配置
a.支持的AI编辑工具
Trae
Cursor
Claude Code
Windsurf
Codebubby
其他支持MCP协议的AI编辑器
b.第一步:启动GVA项目
确保你的GVA项目正在运行,MCP服务会自动在 http://127.0.0.1:8888/sse 启动。
c.第二步:配置AI编辑器
在你的AI编辑工具的配置文件中添加以下MCP配置:
{
"mcpServers": {
"GVA Helper": {
"url": "http://127.0.0.1:8888/sse"
}
}
}
d.第三步:重启编辑器
保存配置后重启你的AI编辑工具,等待MCP连接建立,MCP状态显示绿色即表示连接成功。
03.AI助手新能力
a.配置完成后,AI助手将获得以下超能力:
深度理解项目:自动识别GVA项目结构和代码模式。
智能代码生成:根据需求自动生成完整的功能模块。
精准文件定位:快速找到相关文件并提供修改建议。
全栈开发:同时处理前端、后端、数据库的代码生成。
UI自动化:自动配置路由、菜单和权限系统。
b.使用示例
只需要告诉AI:"我想创建一个用户管理模块",AI就会:
自动生成用户表结构。
创建完整的CRUD API。
生成前端管理页面。
配置菜单和路由。
设置权限控制。
06.配置文件说明
a.yaml
mcp:
name: GVA_MCP # MCP服务名称
version: v1.0.0 # 版本号
sse_path: /sse # SSE路径
message_path: /message # 消息路径
url_prefix: '' # URL前缀
## v2.8.6后可用
addr: 8889 # 监听地址 在separate为true时有效 (暂时未实现独立运行功能,敬请期待)
separate: false # 是否隔离 开启以后mcp将不会跟随gva本体启动 建议在生产环境开启
b.自动填写页面参数示例
点击生成后后端会获得MCP模板。
在模板的handle函数中书写业务逻辑即可实现一个简单的mcp工具。
1.5 Swagger
01.Swagger API 文档
a.功能说明
Swagger 是一个用于设计、构建、记录和使用 RESTful Web 服务的开源软件框架。
Gin-Vue-Admin 集成了 Swagger 来自动生成和维护 API 文档。
b.核心功能
API 文档自动生成:从代码注释自动生成文档。
在线测试:直接在浏览器中测试 API。
可视化界面:清晰的 API 结构展示。
实时更新:代码变更时文档自动同步。
02.安装 Swagger
a.方式一:直接安装(推荐)
# 安装最新版本的 swag 工具
go install github.com/swaggo/swag/cmd/swag@latest
b.方式二:使用代理安装
如果遇到网络问题,建议配置 Go 模块代理:
# 启用 Go Modules
go env -w GO111MODULE=on
# 配置国内代理(选择其一)
go env -w GOPROXY=https://goproxy.cn,direct
# 或者使用
# go env -w GOPROXY=https://goproxy.io,direct
# 安装 swag 工具
go install github.com/swaggo/swag/cmd/swag@latest
c.验证安装
安装完成后,验证 swag 工具是否正确安装:
# 检查 swag 版本
swag --version
# 查看帮助信息
swag --help
03.配置 Swagger 注释
a.主程序注释
在 main.go 文件中添加 API 基本信息:
// @title Gin-Vue-Admin API
// @version 1.0
// @description This is a sample server for Gin-Vue-Admin.
// @termsOfService https://github.com/flipped-aurora/gin-vue-admin
// @contact.name API Support
// @contact.url https://github.com/flipped-aurora/gin-vue-admin/issues
// @contact.email [email protected]
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8888
// @BasePath /
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name x-token
func main() {
// 应用程序代码
}
b.API 接口注释
为每个 API 接口添加详细注释:
// CreateUser 创建用户
// @Tags 用户管理
// @Summary 创建用户
// @Description 创建新的用户账号
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.CreateUserReq true "用户信息"
// @Success 200 {object} response.Response{data=response.UserResponse} "创建成功"
// @Failure 400 {object} response.Response "请求参数错误"
// @Failure 500 {object} response.Response "内部服务器错误"
// @Router /user [post]
func (u *UserApi) CreateUser(c *gin.Context) {
// 接口实现代码
}
c.数据模型注释
为数据结构添加注释:
// User 用户信息
type User struct {
ID uint `json:"id" example:"1"` // 用户ID
Username string `json:"username" example:"admin"` // 用户名
Email string `json:"email" example:"[email protected]"` // 邮箱
Phone string `json:"phone" example:"13800138000"` // 手机号
Status int `json:"status" example:"1"` // 状态:1-启用,0-禁用
}
04.生成 API 文档
a.生成文档
# 在项目根目录(包含 main.go 的目录)下运行,生成 Swagger 文档
swag init
b.生成成功
命令执行成功后,会在项目中生成以下文件:
server/
├── docs/
│ ├── docs.go # 文档数据
│ ├── swagger.json # JSON 格式文档
│ └── swagger.yaml # YAML 格式文档
└── main.go
c.自动化生成
a.您也可以在 main.go 中添加 go:generate 指令来自动化文档生成
//go:generate swag init
package main
b.然后使用 go generate 命令
go generate
05.访问 Swagger 文档
a.启动服务
# 后端服务正在运行:
go run main.go
b.访问文档
# 在浏览器中访问 Swagger UI:
本地访问: http://localhost:8888/swagger/index.html
c.文档功能
Swagger UI 提供以下功能:
API 列表: 查看所有可用的 API 接口。
接口详情: 查看每个接口的参数、响应等详细信息。
在线测试: 直接在页面中测试 API 接口。
下载文档: 下载 JSON 或 YAML 格式的 API 文档。
06.使用技巧
a.接口分组
使用 @Tags 注释对接口进行分组:
// @Tags 用户管理
// @Tags 权限管理
// @Tags 系统设置
b.参数验证
结合 binding 标签进行参数验证:
type CreateUserReq struct {
Username string `json:"username" binding:"required" example:"admin"` // 用户名(必填)
Password string `json:"password" binding:"required,min=6" example:"123456"` // 密码(必填,最少6位)
Email string `json:"email" binding:"email" example:"[email protected]"` // 邮箱(邮箱格式)
}
c.响应示例
提供详细的响应示例:
// @Success 200 {object} response.Response{data=[]model.User} "获取成功"
// @Success 200 {object} response.PageResult{list=[]model.User} "分页获取成功"
d.错误处理
定义常见的错误响应:
// @Failure 400 {object} response.Response "请求参数错误"
// @Failure 401 {object} response.Response "未授权"
// @Failure 403 {object} response.Response "权限不足"
// @Failure 404 {object} response.Response "资源不存在"
// @Failure 500 {object} response.Response "内部服务器错误"
07.高级配置
a.自定义配置
创建 .swaggo 配置文件:
# .swaggo
dir: ./
general_info: main.go
output_dir: ./docs
parse_vendor: false
parse_dependency: false
parse_internal: false
generate_types: false
b.排除特定文件
使用 --exclude 参数排除不需要解析的文件:
swag init --exclude ./vendor
c.指定输出目录
指定输出目录:
swag init --output ./api-docs
08.最佳实践
1.及时更新文档:每次修改 API 后都要重新生成文档。
2.详细注释:为每个接口提供清晰的描述和示例。
3.统一规范:团队内部统一注释格式和命名规范。
4.版本管理:为不同版本的 API 维护对应的文档。
5.测试验证:使用 Swagger UI 测试接口确保文档准确性。
1.6 VsCode
01.快速开始
a.打开工作区
a.推荐使用 VS Code 的工作区功能来管理整个项目
# 进入项目根目录
cd gin-vue-admin
# 使用 VS Code 打开整个项目
code .
b.或者打开预配置的工作区文件
VS Code 工作区。
b.工作区配置
a.创建 .vscode/gin-vue-admin.code-workspace 文件:
{
"folders": [
{
"name": "🔧 Server (Go)",
"path": "./server"
},
{
"name": "🎨 Web (Vue)",
"path": "./web"
},
{
"name": "📚 Docs",
"path": "./docs"
}
],
"settings": {
"go.gopath": "",
"go.goroot": "",
"go.toolsManagement.autoUpdate": true,
"typescript.preferences.importModuleSpecifier": "relative",
"eslint.workingDirectories": ["web"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
},
"extensions": {
"recommendations": [
"golang.go",
"vue.volar",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"ms-vscode.vscode-typescript-next"
]
}
}
02.必备插件安装
a.Go 开发插件
Go (golang.go):Go 语言官方插件,提供语法高亮、智能提示、调试等功能。
Go Outliner (766b.go-outliner):显示 Go 文件的结构大纲。
b.Vue 开发插件
Vue Language Features (Volar) (vue.volar):Vue 3 官方语言支持,替代 Vetur,提供更好的 TypeScript 支持。
TypeScript Vue Plugin (Volar) (vue.vscode-typescript-vue-plugin):Vue 文件的 TypeScript 支持。
c.通用开发插件
Prettier - Code formatter (esbenp.prettier-vscode):代码格式化工具。
ESLint (dbaeumer.vscode-eslint):JavaScript/TypeScript 代码检查。
Tailwind CSS IntelliSense (bradlc.vscode-tailwindcss):Tailwind CSS 智能提示。
Auto Rename Tag (formulahendry.auto-rename-tag):自动重命名配对的 HTML/XML 标签。
Path Intellisense (christian-kohler.path-intellisense):文件路径智能提示。
d.安装插件
使用命令行安装推荐插件:
code --install-extension golang.go
code --install-extension vue.volar
code --install-extension vue.vscode-typescript-vue-plugin
code --install-extension esbenp.prettier-vscode
code --install-extension dbaeumer.vscode-eslint
code --install-extension bradlc.vscode-tailwindcss
code --install-extension formulahendry.auto-rename-tag
code --install-extension christian-kohler.path-intellisense
03.运行和调试配置
a.创建调试配置
在项目根目录创建 .vscode/launch.json 文件:
-----------------------------------------------------------------------------------------------------
{
"version": "0.2.0",
"configurations": [
{
"name": "🔧 Launch Server (Go)",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/server/main.go",
"cwd": "${workspaceFolder}/server",
"env": {
"GIN_MODE": "debug"
},
"args": [],
"showLog": true,
"console": "integratedTerminal"
},
{
"name": "🎨 Launch Web (Node)",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/web",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "serve"]
}
],
"compounds": [
{
"name": "🚀 Launch Both (Server + Web)",
"configurations": [
"🔧 Launch Server (Go)",
"🎨 Launch Web (Node)"
],
"stopAll": true
}
]
}
b.创建任务配置
创建 .vscode/tasks.json 文件:
-----------------------------------------------------------------------------------------------------
{
"version": "2.0.0",
"tasks": [
{
"label": "🔧 Build Server",
"type": "shell",
"command": "go",
"args": ["build", "-o", "gin-vue-admin", "main.go"],
"options": {
"cwd": "${workspaceFolder}/server"
},
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
}
},
{
"label": "🎨 Build Web",
"type": "shell",
"command": "npm",
"args": ["run", "build"],
"options": {
"cwd": "${workspaceFolder}/web"
},
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
}
},
{
"label": "📝 Generate Swagger",
"type": "shell",
"command": "swag",
"args": ["init"],
"options": {
"cwd": "${workspaceFolder}/server"
},
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
}
}
]
}
c.运行项目
a.方式一:使用调试面板
按 Ctrl+Shift+D (Windows/Linux) 或 Cmd+Shift+D (macOS) 打开调试面板。
选择要运行的配置:
Launch Server (Go): 仅启动后端服务。
Launch Web (Node): 仅启动前端应用。
Launch Both (Server + Web): 同时启动前后端。
b.方式二:使用终端
# 启动后端(在 server 目录)
cd server
go run main.go
# 启动前端(在 web 目录)
cd web
npm run serve
c.方式三:使用任务
按 Ctrl+Shift+P (Windows/Linux) 或 Cmd+Shift+P (macOS)。
输入 "Tasks: Run Task"。
选择要执行的任务。
04.Go 环境配置
a.配置 Go 模块代理
为了提高依赖下载速度,建议配置 Go 模块代理:
# 启用 Go Modules
go env -w GO111MODULE=on
# 配置模块代理
go env -w GOPROXY=https://goproxy.cn,direct
# 配置校验和数据库
go env -w GOSUMDB=sum.golang.google.cn
b.VS Code Go 插件配置
在 VS Code 设置中添加以下配置:
{
"go.toolsManagement.autoUpdate": true,
"go.useLanguageServer": true,
"go.gocodeAutoBuild": false,
"go.lintOnSave": "package",
"go.vetOnSave": "package",
"go.buildOnSave": "package",
"go.testOnSave": false,
"go.coverOnSave": false,
"go.formatTool": "goimports",
"go.gotoSymbol.includeImports": true,
"go.gotoSymbol.includeGoroot": true,
"gopls": {
"experimentalWorkspaceModule": true,
"completeUnimported": true,
"usePlaceholders": true,
"deepCompletion": true
}
}
c.安装 Go 工具
在 VS Code 中按 Ctrl+Shift+P,输入 "Go: Install/Update Tools",选择所有工具进行安装。
05.前端开发配置
a.Prettier 配置
在 web 目录创建 .prettierrc 文件:
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "avoid"
}
b.ESLint 配置
确保 web 目录有正确的 .eslintrc.js 配置文件。
c.VS Code 前端设置
在 VS Code 设置中添加以下配置:
{
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.suggest.autoImports": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.workingDirectories": ["web"],
"vetur.validation.template": false,
"vetur.validation.script": false,
"vetur.validation.style": false
}
06.实用技巧
a.代码片段
创建 Go 代码片段 .vscode/go.code-snippets:
{
"Gin Handler": {
"prefix": "ginhandler",
"body": [
"// $1 $2",
"// @Tags $3",
"// @Summary $2",
"// @Security ApiKeyAuth",
"// @accept application/json",
"// @Produce application/json",
"// @Success 200 {object} response.Response \"成功\"",
"// @Router /$4 [$5]",
"func (a *$6Api) $1(c *gin.Context) {",
"\t$0",
"}"
],
"description": "Create a Gin handler with Swagger annotations"
}
}
b.快捷键配置
在 keybindings.json 中添加自定义快捷键:
[
{
"key": "ctrl+shift+r",
"command": "workbench.action.tasks.runTask",
"args": "🔧 Build Server"
},
{
"key": "ctrl+shift+w",
"command": "workbench.action.tasks.runTask",
"args": "🎨 Build Web"
}
]
c.文件关联
在设置中添加文件关联:
{
"files.associations": {
"*.vue": "vue",
"*.go": "go",
"*.yaml": "yaml",
"*.yml": "yaml"
}
}
07.调试技巧
a.Go 调试
在代码行号左侧点击设置断点。
使用 F5 开始调试。
使用 F10 单步执行,F11 步入函数。
在调试控制台查看变量值。
b.前端调试
使用浏览器开发者工具。
在 VS Code 中安装 "Debugger for Chrome" 插件。
配置浏览器调试。
c.日志查看
使用集成终端查看应用日志。
配置输出面板显示不同类型的日志。
08.性能优化
a.排除文件
在 .vscode/settings.json 中排除不必要的文件:
{
"files.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/.git": true,
"**/vendor": true
},
"search.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/vendor": true
}
}
b.禁用不需要的插件
在工作区中禁用不相关的插件以提高性能。
2 代码生成
2.1 创建包名
01.创建package
点击左侧菜单栏的自动化Package进入页面,点击新增打开抽屉
02.填写信息
抽屉中关键属性为包名,此处填写小写字母开头的驼峰式命名单词,这是你自动化代码的基础包,
所有在创建自动化代码时候选择本package的代码,都会创建在由本功能自动创建出的文件夹下。
此处展示以showGva为例,自动生成的文件目录。
03.完成创建
创建完成后,会在web和server下创建对应的package文件夹,
如下所示 web/src/api/showGva web/src/view/showGva server/api/showGva
内含文件 enter.go server/router/showGva
内含文件 enter.go server/service/showGva
内含文件 enter.go
2.2 使用指南
01.代码生成器
a.功能说明
通过代码生成器可以从现有数据库创建代码,选择数据库中的表,然后生成对应的代码。
b.界面名称与生成结构体
Struct名称(StructName):结构体名称,位于 server/model 文件夹下,首字母必须大写。
TableName:指定表名(非必填),数据库中生成的与结构体对应的数据表名。
Struct简称(Abbreviation):简称会作为入参对象名和路由 group。
Struct中文名称(Description):中文描述作为自动api描述。
文件名称(PackageName):生成文件的默认名称,使用小驼峰形式命名。
Package(包):生成的目标包,必选。
业务库(BusinessDB):选择业务库,可选。
使用GVA结构(GvaModel):建议选中。
创建资源标识(AutoCreateResource):可选。
自动创建api(AutoCreateApiToSql):建议选中。
自动创建菜单(AutoCreateMenuToSql):建议选中。
自动移动文件(AutoMoveFile):建议选中。
02.字段界面说明
a.组件内容名称与生成结构体
Field名称(FieldName):结构体名称,首字母大写。
Field中文名(FieldDesc):结构体中文名称。
FieldJSON(FieldJson):golang struct tag json。
数据库字段名(ColumnName):数据库字段名。
Field数据类型(FieldType):字段对应golang数据类型。
数据库字段长度(DataTypeLong):字段数据类型长度。
数据库字段描述(Comment):数据库字段描述。
默认值(DefaultValue):数据库字段默认值。
是否必填(Require):自动创建前后端必填校验。
校验失败文案(ErrorText):必填校验失败的文案。
是否排序(Sort):自动创建前后端排序代码。
前端可见(Front):前端是否创建本字段。
主键(PrimaryKey):数据库主键。
是否可清空(Clearable):前端输入框右侧是否出现X。
Field查询条件(FieldSearchType):搜索类型。
关联字典(DictType):关联字典标记。
数据源配置(dataSource:{table,label,value}):数据源配置。
03.生成一步到位代码包
a.自行设计业务基础结构体模型
点击左侧菜单中的 系统工具 → 代码生成器。
填写好 Struct名称、TableName、Struct简称、Struct中文名称、文件名称。
选择好 自动创建api、自动移动文件 按钮。
点击 新增Field 按钮,为数据表、struct结构体创建字段。
b.从数据库的选择表进行生成结构体
点击左侧菜单中的系统工具 > 代码生成器。
点击 点这里从现有数据库创建代码。
选择 数据库名 以及 表名。
点击使用此表创建。
自行编辑好各个Filed的所需的搜索条件。
c.点击生成代码按钮
完成1.1或1.2步骤操作。
代码会自动移动到前后端你所创建的package文件夹下。
提示:在创建自动化代码时候会自动创建enter.go。
04.注册路由和数据库表
a.注册路由
server/initialize/router.go
初始化总路由。
b.注册数据库表
server/initialize/gorm.go
将你的model结构配置进入 db.AutoMigrate()内部即可。
06.配置目录菜单
a.进入系统 超级管理员 → 菜单管理 菜单,点击 新增根菜单 按钮,配置菜单信息。
略
b.配置项说明
路由name:对应进入列表显示页面时的访问路径。
路由path:选中后边的“添加参数”后才可以输入。
是否隐藏:是否在系统左侧目录菜单显示时,隐藏掉该目录菜单。
父节点Id:该目录菜单的父级目录菜单。
文件路径:对应前端项目中 /view/ PackageName (自建)/StructName.vue 文件。
展示名称:该目录菜单显示在系统左侧目录菜单中的名称。
图标:该目录菜单显示在系统左侧目录菜单中的图标。
排序标记:用于调整该目录菜单在系统左侧目录菜单中显示的上下位置。
keepAlive:是否使用keepAlive缓存。
07.配置后端接口
a.如果在第一步的自动创建api打钩了即可跳过此步。
略
b.进入系统 超级管理员 → api管理 菜单,点击 新增api 按钮,配置接口信息。
略
c.配置项说明
路径:接口路径。
请求:根据接口实际选择。
api分组:对应 struct 简称。
api简介:对api的简要说明。
08.配置角色权限
a.进入系统 超级管理员 → 角色管理 菜单,找到需要设置权限的角色,点击对应的 设置权限 按钮,配置角色相关权限。
略
b.配置项说明
角色菜单:勾选该角色可以访问的目录菜单。
角色api:勾选该角色可以访问的接口。
09.完善新增表单弹窗/页面
a.注意
在 v2.3.5 版本后,不再需要手动创建表单。
b.进入系统 系统工具 → 表单生成器 菜单,根据自己的实际需求,将左侧组件拖拽至中间画布区域,并在右侧设置组件属性。
略
c.配置项说明
组件类型:默认是左侧选中的组件类型。
字段名:对应 Step3 中的 FieldJSON 字段。
标题:即组件label。
占位提示:占位提示。
d.复制代码
点击画布上方的 复制代码 按钮。
选择 生成类型 是 页面 还是 弹窗。
点击 确定 按钮,复制表单代码。
e.代码应用
取出 <el-form>……</el-form> 部分代码,覆盖掉前端项目中 src → view → PackageName(自建) → StructName.vue 中的表单。
将 js 部分data方法里返回的对象复制到前端项目中对应的 .vue 文件的 js 部分。
2.3 常见问题
01.127.0.0.1拒绝连接
a.出现这种情况
旧版本,请到 web/src/view/systemTools/formCreate/index.vue 找到 127.0.0.1 替换为本机或服务器ip
新版本,请前往相对应的环境变量中修改 制定参数 path : VITE_BASE_PATH port :VITE_SERVER_PORT
b.修改对应IP
server项目默认端口是 8888 ,如果你修改,那也要把 127.0.0.1:8888 相应的端口修改 ip:自定义端口
02.生产使用表单生成器
a.需修改web/src/view/systemTools/formCreate/index.vue
<template>
<div style="height:80vh">
<iframe width="100%" height="100%" :src="`${basePath}:${basePort}/form-generator/#/`" frameborder="0" />
</div>
</template>
-----------------------------------------------------------------------------------------------------
修改为
-----------------------------------------------------------------------------------------------------
<template>
<div style="height:80vh">
<iframe width="100%" height="100%" :src="`${basePath}/form-generator/#/`" frameborder="0" />
</div>
</template>
b.添加修改nginx配置
location /form-generator {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8888;
}
c.同时web/.env.production配置为
ENV = 'production'
VITE_CLI_PORT = 8080
VITE_SERVER_PORT = 8888
VITE_BASE_API = /api
#下方修改为你的线上域名
VITE_BASE_PATH = https://你的线上域名
2.4 开发规范指南
01.Go代码规范
a.命名规范
// 包名:小写,简短,有意义
package user
package system
// 常量:大写,下划线分隔
const (
MAX_RETRY_COUNT = 3
DEFAULT_TIMEOUT = 30
API_VERSION = "v1"
)
// 变量:驼峰命名
var (
userService *UserService
configFilePath string
isDebugMode bool
)
// 函数:大写开头(公开),小写开头(私有)
func GetUserList() []User {}
func createUser() error {}
// 结构体:大写开头,驼峰命名
type UserService struct {
db *gorm.DB
redis *redis.Client
}
// 接口:以 -er 结尾或描述性名称
type UserRepository interface {
Create(user *User) error
GetByID(id uint) (*User, error)
}
type Validator interface {
Validate() error
}
b.注释规范
// Package user 提供用户管理相关功能
// 包括用户的增删改查、权限验证等操作
package user
// UserService 用户服务,负责处理用户相关的业务逻辑
type UserService struct {
db *gorm.DB
redis *redis.Client
}
// NewUserService 创建用户服务实例
// 参数:
// db: 数据库连接
// redis: Redis连接
// 返回:
// *UserService: 用户服务实例
func NewUserService(db *gorm.DB, redis *redis.Client) *UserService {
return &UserService{
db: db,
redis: redis,
}
}
// GetUserByID 根据用户ID获取用户信息
// 该方法会先从缓存中查找,如果缓存中不存在则从数据库查询
// 参数:
// id: 用户ID
// 返回:
// *User: 用户信息,如果用户不存在则返回nil
// error: 错误信息
func (s *UserService) GetUserByID(id uint) (*User, error) {
// 实现逻辑...
}
c.错误处理规范
// 自定义错误类型
type UserError struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data,omitempty"`
}
func (e *UserError) Error() string {
return e.Message
}
// 错误常量定义
var (
ErrUserNotFound = &UserError{Code: 1001, Message: "用户不存在"}
ErrUserExists = &UserError{Code: 1002, Message: "用户已存在"}
ErrInvalidPassword = &UserError{Code: 1003, Message: "密码格式不正确"}
ErrPermissionDenied = &UserError{Code: 1004, Message: "权限不足"}
)
// 错误处理示例
func (s *UserService) CreateUser(user *User) error {
// 验证用户是否已存在
exists, err := s.userExists(user.Username)
if err != nil {
return fmt.Errorf("检查用户是否存在失败: %w", err)
}
if exists {
return ErrUserExists
}
// 验证密码强度
if err := s.validatePassword(user.Password); err != nil {
return fmt.Errorf("密码验证失败: %w", err)
}
// 创建用户
if err := s.db.Create(user).Error; err != nil {
return fmt.Errorf("创建用户失败: %w", err)
}
return nil
}
d.结构体标签规范
type User struct {
ID uint `json:"id" gorm:"primarykey;comment:用户ID"`
Username string `json:"username" gorm:"uniqueIndex;size:64;not null;comment:用户名" validate:"required,min=3,max=64"`
Password string `json:"-" gorm:"size:255;not null;comment:密码" validate:"required,min=8"`
Email string `json:"email" gorm:"uniqueIndex;size:128;comment:邮箱" validate:"email"`
Phone string `json:"phone" gorm:"size:20;comment:手机号" validate:"phone"`
Status int `json:"status" gorm:"default:1;comment:状态(1:启用 0:禁用)"`
CreatedAt time.Time `json:"createdAt" gorm:"comment:创建时间"`
UpdatedAt time.Time `json:"updatedAt" gorm:"comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;comment:删除时间"`
}
// TableName 指定表名
func (User) TableName() string {
return "sys_users"
}
02.JavaScript/Vue 代码规范
a.命名规范
// 常量:大写,下划线分隔
const API_BASE_URL = 'http://localhost:8888'
const MAX_FILE_SIZE = 10 * 1024 * 1024
// 变量:驼峰命名
const userName = 'admin'
const isLoggedIn = true
const userList = []
// 函数:驼峰命名,动词开头
function getUserList() {}
function createUser() {}
function validateForm() {}
function handleSubmit() {}
// 类:大写开头,驼峰命名
class UserService {
constructor() {}
async getUsers() {}
async createUser(userData) {}
}
// 组件:大写开头,驼峰命名
const UserList = {
name: 'UserList',
// ...
}
b.Vue 组件规范
<template>
<!-- 使用语义化的HTML标签 -->
<div class="user-list-container">
<!-- 条件渲染使用 v-if/v-show -->
<div v-if="loading" class="loading-wrapper">
<el-loading />
</div>
<!-- 列表渲染使用 v-for,必须添加 key -->
<div
v-for="user in userList"
:key="user.id"
class="user-item"
@click="handleUserClick(user)"
>
{{ user.username }}
</div>
<!-- 事件处理使用方法名,不使用内联表达式 -->
<el-button @click="handleAddUser">添加用户</el-button>
</div>
</template>
<script>
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getUserList, createUser } from '@/api/user'
export default {
name: 'UserList',
// Props 定义要详细
props: {
departmentId: {
type: [String, Number],
required: true,
validator: (value) => value > 0
},
showActions: {
type: Boolean,
default: true
}
},
// Emits 声明
emits: ['user-selected', 'user-created'],
setup(props, { emit }) {
// 响应式数据
const loading = ref(false)
const userList = ref([])
const searchForm = reactive({
keyword: '',
status: 1
})
// 计算属性
const filteredUsers = computed(() => {
return userList.value.filter(user =>
user.username.includes(searchForm.keyword)
)
})
// 方法
const fetchUserList = async () => {
try {
loading.value = true
const { data } = await getUserList({
departmentId: props.departmentId,
...searchForm
})
userList.value = data.list
} catch (error) {
ElMessage.error('获取用户列表失败')
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
const handleUserClick = (user) => {
emit('user-selected', user)
}
const handleAddUser = () => {
// 处理添加用户逻辑
}
// 生命周期
onMounted(() => {
fetchUserList()
})
return {
loading,
userList,
searchForm,
filteredUsers,
fetchUserList,
handleUserClick,
handleAddUser
}
}
}
</script>
<style lang="scss" scoped>
// 使用 BEM 命名规范
.user-list-container {
padding: 20px;
.loading-wrapper {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.user-item {
padding: 10px;
border: 1px solid #e4e7ed;
border-radius: 4px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: #f5f7fa;
border-color: #409eff;
}
&--active {
background-color: #ecf5ff;
border-color: #409eff;
}
}
}
</style>
c.API 调用规范
// api/user.js
import request from '@/utils/request'
// API 函数命名:动词 + 资源名
export function getUserList(params) {
return request({
url: '/user/getUserList',
method: 'post',
data: params
})
}
export function createUser(data) {
return request({
url: '/user/register',
method: 'post',
data
})
}
export function updateUser(id, data) {
return request({
url: `/user/setUserInfo`,
method: 'put',
data: { ...data, ID: id }
})
}
export function deleteUser(id) {
return request({
url: '/user/deleteUser',
method: 'delete',
data: { ID: id }
})
}
// 在组件中使用
import { getUserList, createUser } from '@/api/user'
export default {
setup() {
const fetchUsers = async () => {
try {
const response = await getUserList({ page: 1, pageSize: 10 })
if (response.code === 0) {
userList.value = response.data.list
} else {
ElMessage.error(response.msg || '获取用户列表失败')
}
} catch (error) {
ElMessage.error('网络错误,请稍后重试')
console.error('API调用失败:', error)
}
}
return { fetchUsers }
}
}
03.Git工作流
a.分支管理
# 主要分支
main/master # 主分支,用于生产环境
develop # 开发分支,用于集成开发
# 功能分支
feature/user-management # 功能开发分支
feature/api-optimization # 功能开发分支
# 修复分支
hotfix/login-bug # 紧急修复分支
bugfix/user-validation # 普通修复分支
# 发布分支
release/v1.2.0 # 发布准备分支
b.提交规范
a.提交信息格式
<type>(<scope>): <subject>
<body>
<footer>
b.类型说明
# 功能相关
feat: 新功能
fix: 修复bug
perf: 性能优化
# 代码相关
refactor: 重构代码
style: 代码格式调整
# 文档和测试
docs: 文档更新
test: 测试相关
# 构建和配置
build: 构建系统或依赖更新
ci: CI配置更新
chore: 其他杂项
# 版本相关
revert: 回滚提交
c.提交示例
# 新功能
git commit -m "feat(user): 添加用户批量导入功能
- 支持Excel文件导入
- 添加数据验证
- 支持导入进度显示
Closes #123"
# 修复bug
git commit -m "fix(auth): 修复JWT token过期时间计算错误
修复了token过期时间计算不准确的问题,
现在使用UTC时间进行计算。
Fixes #456"
# 重构
git commit -m "refactor(api): 重构用户API响应结构
BREAKING CHANGE: 用户API响应格式发生变化
- 将user_info改为userInfo
- 添加了权限信息字段"
c.分支操作流程
a.功能开发流程
# 1. 从develop分支创建功能分支
git checkout develop
git pull origin develop
git checkout -b feature/user-management
# 2. 开发功能
# ... 编写代码 ...
# 3. 提交代码
git add .
git commit -m "feat(user): 添加用户管理功能"
# 4. 推送到远程
git push origin feature/user-management
# 5. 创建Pull Request
# 在GitHub/GitLab上创建PR,请求合并到develop分支
# 6. 代码审查通过后合并
# 删除功能分支
git checkout develop
git pull origin develop
git branch -d feature/user-management
git push origin --delete feature/user-management
b.紧急修复流程
# 1. 从main分支创建hotfix分支
git checkout main
git pull origin main
git checkout -b hotfix/login-bug
# 2. 修复问题
# ... 修复代码 ...
# 3. 提交修复
git add .
git commit -m "fix(auth): 修复登录验证失败问题"
# 4. 合并到main和develop
git checkout main
git merge hotfix/login-bug
git push origin main
git checkout develop
git merge hotfix/login-bug
git push origin develop
# 5. 删除hotfix分支
git branch -d hotfix/login-bug
git push origin --delete hotfix/login-bug
# 6. 创建版本标签
git tag -a v1.1.1 -m "修复登录验证问题"
git push origin v1.1.1
04.团队协作
a.代码审查
a.Pull Request 模板
## 变更描述
简要描述本次变更的内容和目的。
## 变更类型
- [ ] 新功能
- [ ] Bug修复
- [ ] 性能优化
- [ ] 重构
- [ ] 文档更新
- [ ] 测试相关
## 测试
- [ ] 单元测试通过
- [ ] 集成测试通过
- [ ] 手动测试通过
- [ ] 性能测试通过(如适用)
## 检查清单
- [ ] 代码符合项目规范
- [ ] 添加了必要的注释
- [ ] 更新了相关文档
- [ ] 没有引入新的安全风险
- [ ] 向后兼容(或已标记为破坏性变更)
## 相关Issue
Closes #123
Related to #456
## 截图(如适用)
<!-- 添加截图或GIF来展示变更效果 -->
## 额外说明
<!-- 任何需要审查者注意的特殊说明 -->
b.代码审查要点
### 功能性审查
- [ ] 功能是否按需求实现
- [ ] 边界条件是否处理
- [ ] 错误处理是否完善
- [ ] 性能是否可接受
### 代码质量审查
- [ ] 代码结构是否清晰
- [ ] 命名是否规范
- [ ] 注释是否充分
- [ ] 是否有代码重复
### 安全性审查
- [ ] 输入验证是否充分
- [ ] 权限控制是否正确
- [ ] 敏感信息是否保护
- [ ] SQL注入等安全问题
### 测试审查
- [ ] 测试覆盖率是否足够
- [ ] 测试用例是否合理
- [ ] 集成测试是否通过
b.项目管理
a.Issue 模板1
## Bug报告
### 环境信息
- 操作系统:
- 浏览器:
- 项目版本:
### 问题描述
简要描述遇到的问题。
### 复现步骤
1. 打开页面
2. 点击按钮
3. 查看结果
### 期望行为
描述期望的正确行为。
### 实际行为
描述实际发生的行为。
### 截图
如果适用,添加截图来帮助解释问题。
### 额外信息
添加任何其他有助于解决问题的信息。
b.Issue 模板2
## 功能需求
### 需求描述
详细描述新功能的需求。
### 用户故事
作为 [用户角色],我希望 [功能描述],以便 [价值/目的]。
### 验收标准
- [ ] 标准1
- [ ] 标准2
- [ ] 标准3
### 设计稿
<!-- 如果有设计稿,请添加链接或图片 -->
### 技术要求
- 前端技术栈:
- 后端技术栈:
- 数据库变更:
### 优先级
- [ ] 高
- [ ] 中
- [ ] 低
05.开发工具配置
a.VS Code 配置
a.settings.json
{
"go.formatTool": "goimports",
"go.lintTool": "golangci-lint",
"go.testFlags": ["-v"],
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"vue"
],
"vetur.format.defaultFormatter.html": "prettier",
"vetur.format.defaultFormatter.js": "prettier",
"vetur.format.defaultFormatter.css": "prettier"
}
b.推荐扩展
{
"recommendations": [
"golang.go",
"vue.volar",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-json",
"redhat.vscode-yaml"
]
}
b.Git Hooks
a.pre-commit
#!/bin/sh
# .git/hooks/pre-commit
echo "Running pre-commit checks..."
# 检查Go代码格式
if ! gofmt -l . | grep -q '^$'; then
echo "Go code is not formatted. Please run 'gofmt -w .'"
exit 1
fi
# 运行Go测试
if ! go test ./...; then
echo "Go tests failed"
exit 1
fi
# 检查前端代码格式
cd web
if ! npm run lint; then
echo "Frontend linting failed"
exit 1
fi
echo "Pre-commit checks passed!"
b.commit-msg
#!/bin/sh
# .git/hooks/commit-msg
commit_regex='^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(:markdown-math{encoded=".%2B"})?: .{1,50}'
if ! grep -qE ":markdown-math{single="true" encoded="commit_regex%22%20%22"}1"; then
echo "Invalid commit message format!"
echo "Format: <type>(<scope>): <subject>"
echo "Example: feat(user): add user registration"
exit 1
fi
06.代码质量工具
a.golangci.yml
linters-settings:
govet:
check-shadowing: true
golint:
min-confidence: 0
gocyclo:
min-complexity: 15
maligned:
suggest-new: true
dupl:
threshold: 100
goconst:
min-len: 2
min-occurrences: 2
misspell:
locale: US
lll:
line-length: 140
goimports:
local-prefixes: github.com/flipped-aurora/gin-vue-admin
linters:
enable:
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
- errcheck
- gochecknoinits
- goconst
- gocyclo
- gofmt
- goimports
- golint
- gosec
- gosimple
- govet
- ineffassign
- interfacer
- lll
- misspell
- nakedret
- scopelint
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace
run:
timeout: 5m
b.eslintrc.js
module.exports = {
root: true,
env: {
node: true,
browser: true,
es2021: true
},
extends: [
'plugin:vue/vue3-essential',
'@vue/standard'
],
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/multi-word-component-names': 'off',
'space-before-function-paren': ['error', 'never'],
'comma-dangle': ['error', 'never']
}
}
07.质量保证
a.测试规范
a.单元测试
// user_service_test.go
package service
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestUserService_CreateUser(t *testing.T) {
// 准备测试数据
user := &User{
Username: "testuser",
Email: "[email protected]",
Password: "password123",
}
// 创建mock
mockDB := new(MockDB)
mockDB.On("Create", mock.AnythingOfType("*User")).Return(nil)
// 创建服务实例
service := NewUserService(mockDB, nil)
// 执行测试
err := service.CreateUser(user)
// 断言结果
assert.NoError(t, err)
mockDB.AssertExpectations(t)
}
func TestUserService_CreateUser_UserExists(t *testing.T) {
user := &User{
Username: "existinguser",
Email: "[email protected]",
Password: "password123",
}
mockDB := new(MockDB)
mockDB.On("First", mock.AnythingOfType("*User"), mock.Anything).Return(nil)
service := NewUserService(mockDB, nil)
err := service.CreateUser(user)
assert.Error(t, err)
assert.Equal(t, ErrUserExists, err)
}
b.集成测试
// integration_test.go
package test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestUserAPI_CreateUser(t *testing.T) {
// 设置测试环境
gin.SetMode(gin.TestMode)
router := setupRouter()
// 准备测试数据
userData := map[string]interface{}{
"username": "testuser",
"email": "[email protected]",
"password": "password123",
}
jsonData, _ := json.Marshal(userData)
// 创建请求
req, _ := http.NewRequest("POST", "/user/register", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
// 执行请求
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// 验证响应
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, float64(0), response["code"])
}
b.性能测试
// benchmark_test.go
package service
import (
"testing"
)
func BenchmarkUserService_GetUserByID(b *testing.B) {
service := setupUserService()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := service.GetUserByID(1)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkUserService_GetUserList(b *testing.B) {
service := setupUserService()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, err := service.GetUserList(PageInfo{Page: 1, PageSize: 10})
if err != nil {
b.Fatal(err)
}
}
}
2.5 常见问题解答
01.安装和启动问题
a.后端启动问题
a.问题:数据库连接失败
a.错误信息
failed to initialize database, got error dial tcp 127.0.0.1:3306: connect: connection refused
b.解决方案
a.检查数据库服务是否启动
# MySQL
sudo systemctl status mysql
# 或
brew services list | grep mysql
# PostgreSQL
sudo systemctl status postgresql
# 或
brew services list | grep postgresql
b.检查配置文件 config.yaml
mysql:
path: 127.0.0.1
port: "3306"
config: charset=utf8mb4&parseTime=True&loc=Local
db-name: gin_vue_admin
username: root
password: "your_password"
prefix: ""
singular: false
engine: ""
max-idle-conns: 10
max-open-conns: 100
log-mode: ""
log-zap: false
c.测试数据库连接
mysql -h 127.0.0.1 -P 3306 -u root -p
d.创建数据库
CREATE DATABASE gin_vue_admin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
b.问题:Redis 连接失败
a.错误信息
failed to ping redis, got error dial tcp 127.0.0.1:6379: connect: connection refused
b.解决方案
a.启动 Redis 服务
# Linux
sudo systemctl start redis
# macOS
brew services start redis
# 手动启动
redis-server
b.检查 Redis 配置
redis:
db: 0
addr: 127.0.0.1:6379
password: ""
c.测试 Redis 连接
redis-cli ping
c.问题:端口被占用
a.错误信息
listen tcp :8888: bind: address already in use
b.解决方案
a.查找占用端口的进程
# Linux/macOS
lsof -i :8888
netstat -tulpn | grep 8888
# Windows
netstat -ano | findstr 8888
b.终止占用进程
# Linux/macOS
kill -9 <PID>
# Windows
taskkill /PID <PID> /F
c.或修改配置文件中的端口
system:
addr: 8889 # 修改为其他端口
b.前端启动问题
a.问题:依赖安装失败
a.错误信息
npm ERR! code ERESOLVECONFLICT
npm ERR! ERESOLVECONFLICT unable to resolve dependency tree
b.解决方案
a.清理缓存和依赖
# 删除 node_modules 和 package-lock.json
rm -rf node_modules package-lock.json
# 清理 npm 缓存
npm cache clean --force
# 重新安装
npm install
b.使用 yarn 替代 npm
# 安装 yarn
npm install -g yarn
# 使用 yarn 安装依赖
yarn install
c.使用 --legacy-peer-deps 参数
npm install --legacy-peer-deps
b.问题:Node.js 版本不兼容
a.错误信息
engine "node" is incompatible with this module
b.解决方案
a.检查 Node.js 版本
node --version
b.安装推荐版本(Node.js 16+)
# 使用 nvm 管理 Node.js 版本
nvm install 18
nvm use 18
c.或修改 package.json 中的引擎要求
{
"engines": {
"node": ">=14.0.0"
}
}
c.问题:Vite 开发服务器启动失败
a.错误信息
Error: Cannot find module '@vitejs/plugin-vue'
b.解决方案
a.重新安装 Vite 相关依赖
npm install @vitejs/plugin-vue @vitejs/plugin-vue-jsx --save-dev
b.检查 vite.config.js 配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:8888',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
02.开发问题
a.API 请求问题
a.问题:跨域请求被阻止
a.错误信息
Access to XMLHttpRequest at 'http://localhost:8888/api/login' from origin 'http://localhost:8080' has been blocked by CORS policy
b.解决方案
a.后端配置 CORS 中间件
// middleware/cors.go
func Cors() gin.HandlerFunc {
return gin.HandlerFunc(func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
if method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
}
b.前端配置代理
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8888',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
b.问题:Token 验证失败
a.错误信息
{
"code": 7,
"data": {},
"msg": "token过期"
}
b.解决方案
a.检查 Token 是否正确设置
// utils/request.js
service.interceptors.request.use(
config => {
const token = store.getters['user/token']
if (token) {
config.headers['x-token'] = token
}
return config
},
error => {
return Promise.reject(error)
}
)
b.实现 Token 自动刷新
// utils/request.js
service.interceptors.response.use(
response => {
const res = response.data
if (res.code === 7) {
// Token 过期,尝试刷新
return store.dispatch('user/refreshToken').then(() => {
// 重新发送原请求
return service(response.config)
}).catch(() => {
// 刷新失败,跳转登录页
store.dispatch('user/logout')
router.push('/login')
return Promise.reject(new Error('Token 过期'))
})
}
return response
},
error => {
return Promise.reject(error)
}
)
b.权限问题
a.问题:权限验证失败
a.错误信息
{
"code": 7,
"data": {},
"msg": "权限不足"
}
b.解决方案
a.检查用户角色权限配置
-- 查看用户权限
SELECT u.username, a.authority_name, ar.authority_id, ar.path, ar.method
FROM sys_users u
JOIN sys_authorities a ON u.authority_id = a.authority_id
JOIN sys_authority_menus am ON a.authority_id = am.sys_authority_authority_id
JOIN sys_base_menus m ON am.sys_base_menu_id = m.id
JOIN casbin_rule ar ON a.authority_id = ar.v0
WHERE u.username = 'your_username';
b.添加权限规则
// 在系统管理 -> 角色管理 -> API权限中添加对应的权限
// 或通过代码添加
casbinService := service.ServiceGroupApp.SystemServiceGroup.CasbinService
casbinService.UpdateCasbin(authorityId, casbinInfos)
c.检查 Casbin 配置
// config/casbin.go
func (c *Casbin) TableName() string {
return c.CasbinRule
}
c.数据库问题
a.问题:数据库迁移失败
a.错误信息
Error 1071: Specified key was too long; max key length is 767 bytes
b.解决方案
a.修改 MySQL 配置
-- 设置 innodb_large_prefix
SET GLOBAL innodb_large_prefix = 1;
SET GLOBAL innodb_file_format = 'Barracuda';
SET GLOBAL innodb_file_per_table = 1;
b.修改表结构
// 在模型中指定索引长度
type SysUser struct {
Username string `gorm:"index:idx_username,length:191;comment:用户登录名"`
Email string `gorm:"index:idx_email,length:191;comment:用户邮箱"`
}
b.问题:外键约束错误
a.错误信息
Error 1452: Cannot add or update a child row: a foreign key constraint fails
b.解决方案
a.检查外键关联的数据是否存在
-- 检查关联数据
SELECT * FROM sys_authorities WHERE authority_id = 'your_authority_id';
b.临时禁用外键检查
SET FOREIGN_KEY_CHECKS = 0;
-- 执行你的操作
SET FOREIGN_KEY_CHECKS = 1;
c.修改 GORM 配置
// config/gorm.go
func GormConfig() *gorm.Config {
return &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
// 其他配置...
}
}
03.前端问题
a.路由问题
a.问题:路由跳转失败
a.错误信息
Uncaught (in promise) NavigationDuplicated: Avoided redundant navigation to current location
b.解决方案
a.检查路由跳转逻辑
// 避免重复跳转
if (this.$route.path !== '/target-path') {
this.$router.push('/target-path')
}
b.使用 replace 替代 push
this.$router.replace('/target-path')
b.问题:动态路由不生效
a.解决方案
a.检查路由注册顺序
// router/index.js
// 确保动态路由在静态路由之后注册
const routes = [
// 静态路由
{ path: '/login', component: Login },
// 动态路由
...asyncRoutes
]
b.检查权限验证逻辑
// store/modules/router.js
const actions = {
async GenerateRoutes({ commit }, roles) {
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
commit('SET_ROUTES', accessedRoutes)
return accessedRoutes
}
}
b.组件问题
a.问题:Element Plus 组件样式异常
a.解决方案:
a.检查样式导入
// main.js
import 'element-plus/dist/index.css'
b.检查主题配置
// styles/element-variables.scss
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #409eff,
),
)
);
b.问题:组件响应式失效
a.解决方案
a.检查数据响应式声明
// Vue 3 Composition API
import { ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const state = reactive({
name: 'test'
})
return {
count,
state
}
}
}
b.检查数组/对象更新方式
// 错误方式
this.list[0] = newItem
// 正确方式
this.$set(this.list, 0, newItem)
// 或
this.list.splice(0, 1, newItem)
04.部署问题
a.Docker 部署问题
a.问题:Docker 镜像构建失败
a.错误信息
failed to solve with frontend dockerfile.v0: failed to read dockerfile
b.解决方案
a.检查 Dockerfile 语法
# 确保 Dockerfile 格式正确
FROM golang:1.19-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o main .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
COPY --from=builder /app/config.yaml .
CMD ["./main"]
b.检查 .dockerignore 文件
node_modules
.git
.gitignore
README.md
Dockerfile
.dockerignore
b.问题:容器启动失败
a.错误信息
standard_init_linux.go: exec user process caused: no such file or directory
b.解决方案
a.检查可执行文件权限
RUN chmod +x ./main
b.使用正确的基础镜像
# 对于 Go 程序,使用 alpine 或 scratch
FROM alpine:latest
# 或
FROM scratch
b.Nginx 配置问题
a.问题:前端路由 404
a.解决方案
server {
listen 80;
server_name your-domain.com;
root /var/www/html;
index index.html;
# 处理前端路由
location / {
try_files $uri $uri/ /index.html;
}
# API 代理
location /api {
proxy_pass http://backend:8888;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
b.问题:静态资源加载失败
a.解决方案
server {
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# 字体文件
location ~* \.(woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public";
add_header Access-Control-Allow-Origin "*";
}
}
05.调试技巧
a.后端调试
a.启用详细日志
# config.yaml
zap:
level: 'debug' # 设置为 debug 级别
format: 'console'
prefix: '[gin-vue-admin]'
director: 'log'
show-line: true
encode-level: 'LowercaseColorLevelEncoder'
stacktrace-key: 'stacktrace'
log-in-console: true
b.使用 Delve 调试器
# 安装 Delve
go install github.com/go-delve/delve/cmd/dlv@latest
# 启动调试
dlv debug main.go
# 在代码中设置断点
(dlv) break main.main
(dlv) continue
b.前端调试
a.使用 Vue DevTools
安装浏览器扩展
在开发环境中启用
// main.js
if (process.env.NODE_ENV === 'development') {
app.config.devtools = true
}
b.网络请求调试
// utils/request.js
service.interceptors.request.use(
config => {
console.log('Request:', config)
return config
}
)
service.interceptors.response.use(
response => {
console.log('Response:', response)
return response
},
error => {
console.error('Request Error:', error)
return Promise.reject(error)
}
)
06.性能问题
a.后端性能优化
a.数据库查询优化
// 使用索引
db.Where("username = ?", username).First(&user)
// 预加载关联数据
db.Preload("Authorities").Find(&users)
// 分页查询
db.Limit(pageSize).Offset(offset).Find(&users)
// 只查询需要的字段
db.Select("id, username, email").Find(&users)
b.缓存优化
// 使用 Redis 缓存
func GetUserFromCache(userID uint) (*system.SysUser, error) {
key := fmt.Sprintf("user:%d", userID)
cached := global.GVA_REDIS.Get(context.Background(), key).Val()
if cached != "" {
var user system.SysUser
err := json.Unmarshal([]byte(cached), &user)
return &user, err
}
// 从数据库查询并缓存
var user system.SysUser
err := global.GVA_DB.First(&user, userID).Error
if err != nil {
return nil, err
}
userJSON, _ := json.Marshal(user)
global.GVA_REDIS.Set(context.Background(), key, userJSON, 30*time.Minute)
return &user, nil
}
b.前端性能优化
a.组件懒加载
// 路由懒加载
const routes = [
{
path: '/dashboard',
component: () => import('@/views/dashboard/index.vue')
}
]
// 组件懒加载
export default {
components: {
HeavyComponent: () => import('@/components/HeavyComponent.vue')
}
}
b.虚拟滚动
<template>
<el-table
v-loading="loading"
:data="visibleData"
height="400"
@scroll="handleScroll"
>
<!-- 表格列定义 -->
</el-table>
</template>
<script>
export default {
data() {
return {
allData: [],
visibleData: [],
scrollTop: 0,
itemHeight: 50,
containerHeight: 400
}
},
computed: {
startIndex() {
return Math.floor(this.scrollTop / this.itemHeight)
},
endIndex() {
const visibleCount = Math.ceil(this.containerHeight / this.itemHeight)
return Math.min(this.startIndex + visibleCount, this.allData.length)
}
},
watch: {
startIndex() {
this.updateVisibleData()
},
endIndex() {
this.updateVisibleData()
}
},
methods: {
updateVisibleData() {
this.visibleData = this.allData.slice(this.startIndex, this.endIndex)
},
handleScroll(event) {
this.scrollTop = event.target.scrollTop
}
}
}
</script>
07.获取帮助
a.提交 Bug 报告
a.环境信息
操作系统版本
Go 版本
Node.js 版本
数据库版本
b.错误描述
详细的错误信息
复现步骤
期望行为
c.相关代码
最小复现代码
配置文件
日志信息
b.常用调试命令
# 查看 Go 版本
go version
# 查看 Node.js 版本
node --version
npm --version
# 查看数据库版本
mysql --version
redis-server --version
# 查看端口占用
lsof -i :8888
netstat -tulpn | grep 8888
# 查看进程
ps aux | grep gin-vue-admin
# 查看日志
tail -f log/server.log
journalctl -u gin-vue-admin -f
# Docker 相关
docker ps
docker logs container_name
docker exec -it container_name /bin/sh
3 前端管理
3.1 操作指南
01.技术栈
a.核心框架
Vue 3:渐进式 JavaScript 框架
Vite 4:下一代前端构建工具
Element Plus:基于 Vue 3 的组件库
b.状态管理
Pinia:Vue 3 官方推荐的状态管理库
Vue Router 4:Vue.js 官方路由管理器
c.开发工具
TypeScript:JavaScript 的超集(可选)
ESLint:代码质量检查工具
Prettier:代码格式化工具
Sass/SCSS:CSS 预处理器
d.构建优化
Vite Plugin:丰富的插件生态
Tree Shaking:自动移除未使用代码
Code Splitting:代码分割优化
Hot Module Replacement:热模块替换
02.前端目录结构
web
├── babel.config.js
├── Dockerfile
├── favicon.ico
├── index.html -- 主页面
├── limit.js -- 助手代码
├── package.json -- 包管理器代码
├── src -- 源代码
│ ├── api -- api 组
│ ├── App.vue -- 主页面
│ ├── assets -- 静态资源
│ ├── components -- 全局组件
│ ├── core -- gva 组件包
│ │ ├── config.js -- gva网站配置文件
│ │ ├── gin-vue-admin.js -- 注册欢迎文件
│ │ └── global.js -- 统一导入文件
│ ├── directive -- v-auth 注册文件
│ ├── main.js -- 主文件
│ ├── permission.js -- 路由中间件
│ ├── pinia -- pinia 状态管理器,取代vuex
│ │ ├── index.js -- 入口文件
│ │ └── modules -- modules
│ │ ├── dictionary.js
│ │ ├── router.js
│ │ └── user.js
│ ├── router -- 路由声明文件
│ │ └── index.js
│ ├── style -- 全局样式
│ │ ├── base.scss
│ │ ├── basics.scss
│ │ ├── element_visiable.scss -- 此处可以全局覆盖 element-plus 样式
│ │ ├── iconfont.css -- 顶部几个icon的样式文件
│ │ ├── main.scss
│ │ ├── mobile.scss
│ │ └── newLogin.scss
│ ├── utils -- 方法包库
│ │ ├── asyncRouter.js -- 动态路由相关
│ │ ├── btnAuth.js -- 动态权限按钮相关
│ │ ├── bus.js -- 全局mitt声明文件
│ │ ├── date.js -- 日期相关
│ │ ├── dictionary.js -- 获取字典方法
│ │ ├── downloadImg.js -- 下载图片方法
│ │ ├── format.js -- 格式整理相关
│ │ ├── image.js -- 图片相关方法
│ │ ├── page.js -- 设置页面标题
│ │ ├── request.js -- 统一请求文件
│ │ └── stringFun.js -- 字符串文件
| ├── view -- 主要view代码
| | ├── about -- 关于我们
| | ├── dashboard -- 面板
| | ├── error -- 错误
| | ├── example -- 上传案例
| | ├── iconList -- icon列表
| | ├── init -- 初始化数据
| | ├── layout -- layout约束页面
| | | ├── aside -- 侧边栏
| | | ├── bottomInfo -- bottomInfo
| | | ├── screenfull -- 全屏设置
| | | ├── setting -- 系统设置
| | | └── index.vue -- base 约束
| | ├── login --登录
| | ├── person --个人中心
| | ├── superAdmin -- 超级管理员操作
| | ├── system -- 系统检测页面
| | ├── systemTools -- 系统配置相关页面
| | └── routerHolder.vue -- page 入口页面
├── vite.config.js -- vite 配置文件
└── yarn.lock
03.开发环境配置
a.环境要求
Node.js >= 16.0.0
npm >= 8.0.0 或 yarn >= 1.22.0
Git版本控制工具
b.安装依赖
# 进入前端目录
cd web
# 使用npm安装
npm install
# 或使用yarn安装
c.开发命令
# 启动开发服务器
npm run serve
# 或
yarn serve
# 构建生产版本
npm run build
# 或
yarn build
# 代码检查
npm run lint
# 或
yarn lint
# 代码格式化
npm run format
# 或
yarn format
04.核心配置文件
a.Vite配置 (vite.config.js)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export default defineConfig({
plugins: [
vue(),
createSvgIconsPlugin({
iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
symbolId: 'icon-[dir]-[name]'
})
],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 8080,
proxy: {
'/api': {
target: 'http://127.0.0.1:8888',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets',
rollupOptions: {
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]'
}
}
}
})
b.项目配置 (src/core/config.js)
// 系统全局配置
export const config = {
appName: 'Gin-Vue-Admin',
appLogo: 'logoIco.png',
showProgressBar: true,
progressBarColor: '#409EFF',
showInfoTip: true,
// 布局配置
layout: {
showTagsView: true,
showSidebarLogo: true,
fixedHeader: true,
sidebarTextTheme: true,
showGreyMode: false,
showColorWeakness: false
},
// 主题配置
theme: {
primaryColor: '#409EFF',
successColor: '#67C23A',
warningColor: '#E6A23C',
dangerColor: '#F56C6C',
infoColor: '#909399'
}
}
05.核心架构
a.路由系统
a.静态路由配置
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { asyncRouterHandle } from '@/utils/asyncRouter'
// 静态路由
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/view/login/index.vue')
},
{
path: '/',
redirect: '/dashboard'
},
{
path: '/layout',
name: 'Layout',
component: () => import('@/view/layout/index.vue'),
children: [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/view/dashboard/index.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
b.动态路由处理
// src/utils/asyncRouter.js
import { asyncRoutes } from '@/router/asyncRouter'
// 动态路由处理
export function asyncRouterHandle(asyncRouter) {
asyncRouter.forEach(item => {
if (item.component) {
if (item.component === 'view/routerHolder.vue') {
item.component = () => import('@/view/routerHolder.vue')
} else {
const component = item.component
item.component = () => import(`@/view/${component}`)
}
}
if (item.children) {
asyncRouterHandle(item.children)
}
})
return asyncRouter
}
// 格式化路由
export function formatRouter(routes, routeMap) {
const newRoutes = []
routes.forEach(item => {
if (item.path === 'dashboard') {
item.component = () => import('@/view/dashboard/index.vue')
} else if (item.component) {
item.component = routeMap[item.component] || (() => import(`@/view/error/404.vue`))
}
if (item.children && item.children.length > 0) {
item.children = formatRouter(item.children, routeMap)
}
newRoutes.push(item)
})
return newRoutes
}
b.状态管理 (Pinia)
a.用户状态管理
// src/pinia/modules/user.js
import { defineStore } from 'pinia'
import { login, getUserInfo, logout } from '@/api/user'
import { jsonInBlacklist } from '@/api/jwt'
import router from '@/router/index'
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: {
uuid: '',
nickName: '',
headerImg: '',
authority: {},
sideMode: 'dark',
activeColor: '#1890ff',
baseColor: '#fff'
},
token: '',
mode: 'light'
}),
getters: {
// 获取用户信息
getUserInfo: (state) => state.userInfo,
// 获取token
getToken: (state) => state.token,
// 获取模式
getMode: (state) => state.mode
},
actions: {
// 登录
async LoginIn(loginInfo) {
try {
const res = await login(loginInfo)
if (res.code === 0) {
this.setUserInfo(res.data.user)
this.setToken(res.data.token)
await this.GetUserInfo()
return true
}
} catch (error) {
console.error('登录失败:', error)
return false
}
},
// 获取用户信息
async GetUserInfo() {
try {
const res = await getUserInfo()
if (res.code === 0) {
this.setUserInfo(res.data.userInfo)
}
return res
} catch (error) {
console.error('获取用户信息失败:', error)
}
},
// 登出
async LoginOut() {
try {
const res = await logout()
if (res.code === 0) {
this.userInfo = {}
this.token = ''
localStorage.clear()
router.push({ name: 'Login' })
}
} catch (error) {
console.error('登出失败:', error)
}
},
// 设置用户信息
setUserInfo(userInfo) {
this.userInfo = { ...this.userInfo, ...userInfo }
},
// 设置token
setToken(token) {
this.token = token
localStorage.setItem('token', token)
},
// 设置模式
setMode(mode) {
this.mode = mode
localStorage.setItem('mode', mode)
}
}
})
b.路由状态管理
// src/pinia/modules/router.js
import { defineStore } from 'pinia'
import { asyncRouterHandle } from '@/utils/asyncRouter'
import { getMenu } from '@/api/menu'
export const useRouterStore = defineStore('router', {
state: () => ({
asyncRouters: [],
keepAliveRouters: [],
routerList: [],
addRouters: [],
routerMap: {}
}),
actions: {
// 设置动态路由
async SetAsyncRouter() {
try {
const res = await getMenu()
if (res.code === 0) {
const asyncRouter = res.data.menus || []
this.asyncRouters = asyncRouterHandle(asyncRouter)
this.routerList = res.data.menus || []
return this.asyncRouters
}
} catch (error) {
console.error('获取菜单失败:', error)
}
},
// 设置keep-alive路由
setKeepAliveRouters(history) {
this.keepAliveRouters = history
}
}
})
c.HTTP请求封装
// src/utils/request.js
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/pinia/modules/user'
import router from '@/router/index'
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_BASE_API,
timeout: 99999
})
// 请求拦截器
service.interceptors.request.use(
config => {
const userStore = useUserStore()
// 添加token
if (userStore.token) {
config.headers['x-token'] = userStore.token
}
// 添加请求时间戳
config.headers['x-timestamp'] = Date.now()
return config
},
error => {
console.error('请求错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data
// 处理文件下载
if (response.headers['content-type'] === 'application/octet-stream') {
return response
}
// 业务错误处理
if (res.code !== 0) {
ElMessage({
message: res.msg || '请求失败',
type: 'error',
duration: 5 * 1000
})
// token过期处理
if (res.code === 1004 || res.code === 1005) {
const userStore = useUserStore()
userStore.LoginOut()
return Promise.reject(new Error(res.msg || '登录过期'))
}
return Promise.reject(new Error(res.msg || '请求失败'))
}
return res
},
error => {
console.error('响应错误:', error)
let message = '网络错误'
if (error.response) {
switch (error.response.status) {
case 401:
message = '未授权,请重新登录'
break
case 403:
message = '权限不足'
break
case 404:
message = '请求的资源不存在'
break
case 500:
message = '服务器内部错误'
break
default:
message = `连接错误${error.response.status}`
}
}
ElMessage({
message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
06.组件开发
a.全局组件注册
// src/core/global.js
import GvaIcon from '@/components/gva-icon/index.vue'
import GvaTable from '@/components/gva-table/index.vue'
import GvaForm from '@/components/gva-form/index.vue'
import GvaUpload from '@/components/gva-upload/index.vue'
// 全局组件列表
const components = {
GvaIcon,
GvaTable,
GvaForm,
GvaUpload
}
// 注册全局组件
export function setupGlobalComponents(app) {
Object.keys(components).forEach(key => {
app.component(key, components[key])
})
}
b.自定义组件示例
<!-- src/components/gva-table/index.vue -->
<template>
<div class="gva-table">
<el-table
ref="tableRef"
v-loading="loading"
:data="tableData"
:height="height"
:max-height="maxHeight"
:stripe="stripe"
:border="border"
:size="size"
:fit="fit"
:show-header="showHeader"
:highlight-current-row="highlightCurrentRow"
:current-row-key="currentRowKey"
:row-class-name="rowClassName"
:row-style="rowStyle"
:cell-class-name="cellClassName"
:cell-style="cellStyle"
:header-row-class-name="headerRowClassName"
:header-row-style="headerRowStyle"
:header-cell-class-name="headerCellClassName"
:header-cell-style="headerCellStyle"
:row-key="rowKey"
:empty-text="emptyText"
:default-expand-all="defaultExpandAll"
:expand-row-keys="expandRowKeys"
:default-sort="defaultSort"
:tooltip-effect="tooltipEffect"
:show-summary="showSummary"
:sum-text="sumText"
:summary-method="summaryMethod"
:span-method="spanMethod"
:select-on-indeterminate="selectOnIndeterminate"
:indent="indent"
:lazy="lazy"
:load="load"
:tree-props="treeProps"
@select="handleSelect"
@select-all="handleSelectAll"
@selection-change="handleSelectionChange"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
@cell-click="handleCellClick"
@cell-dblclick="handleCellDblclick"
@row-click="handleRowClick"
@row-contextmenu="handleRowContextmenu"
@row-dblclick="handleRowDblclick"
@header-click="handleHeaderClick"
@header-contextmenu="handleHeaderContextmenu"
@sort-change="handleSortChange"
@filter-change="handleFilterChange"
@current-change="handleCurrentChange"
@header-dragend="handleHeaderDragend"
@expand-change="handleExpandChange"
>
<slot />
</el-table>
<!-- 分页组件 -->
<div v-if="showPagination" class="gva-pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="pageSizes"
:small="small"
:disabled="disabled"
:background="background"
:layout="layout"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// Props定义
const props = defineProps({
// 表格数据
tableData: {
type: Array,
default: () => []
},
// 加载状态
loading: {
type: Boolean,
default: false
},
// 是否显示分页
showPagination: {
type: Boolean,
default: true
},
// 分页配置
total: {
type: Number,
default: 0
},
currentPage: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
pageSizes: {
type: Array,
default: () => [10, 20, 50, 100]
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
// 表格配置
height: [String, Number],
maxHeight: [String, Number],
stripe: Boolean,
border: Boolean,
size: String,
fit: {
type: Boolean,
default: true
},
showHeader: {
type: Boolean,
default: true
},
highlightCurrentRow: Boolean,
currentRowKey: [String, Number],
rowClassName: [String, Function],
rowStyle: [Object, Function],
cellClassName: [String, Function],
cellStyle: [Object, Function],
headerRowClassName: [String, Function],
headerRowStyle: [Object, Function],
headerCellClassName: [String, Function],
headerCellStyle: [Object, Function],
rowKey: [String, Function],
emptyText: String,
defaultExpandAll: Boolean,
expandRowKeys: Array,
defaultSort: Object,
tooltipEffect: String,
showSummary: Boolean,
sumText: String,
summaryMethod: Function,
spanMethod: Function,
selectOnIndeterminate: {
type: Boolean,
default: true
},
indent: {
type: Number,
default: 16
},
lazy: Boolean,
load: Function,
treeProps: {
type: Object,
default: () => ({
hasChildren: 'hasChildren',
children: 'children'
})
},
small: Boolean,
disabled: Boolean,
background: {
type: Boolean,
default: true
}
})
// Emits定义
const emit = defineEmits([
'select',
'select-all',
'selection-change',
'cell-mouse-enter',
'cell-mouse-leave',
'cell-click',
'cell-dblclick',
'row-click',
'row-contextmenu',
'row-dblclick',
'header-click',
'header-contextmenu',
'sort-change',
'filter-change',
'current-change',
'header-dragend',
'expand-change',
'size-change',
'page-change'
])
// 表格引用
const tableRef = ref()
// 事件处理
const handleSelect = (selection, row) => emit('select', selection, row)
const handleSelectAll = (selection) => emit('select-all', selection)
const handleSelectionChange = (selection) => emit('selection-change', selection)
const handleCellMouseEnter = (row, column, cell, event) => emit('cell-mouse-enter', row, column, cell, event)
const handleCellMouseLeave = (row, column, cell, event) => emit('cell-mouse-leave', row, column, cell, event)
const handleCellClick = (row, column, cell, event) => emit('cell-click', row, column, cell, event)
const handleCellDblclick = (row, column, cell, event) => emit('cell-dblclick', row, column, cell, event)
const handleRowClick = (row, column, event) => emit('row-click', row, column, event)
const handleRowContextmenu = (row, column, event) => emit('row-contextmenu', row, column, event)
const handleRowDblclick = (row, column, event) => emit('row-dblclick', row, column, event)
const handleHeaderClick = (column, event) => emit('header-click', column, event)
const handleHeaderContextmenu = (column, event) => emit('header-contextmenu', column, event)
const handleSortChange = (data) => emit('sort-change', data)
const handleFilterChange = (filters) => emit('filter-change', filters)
const handleCurrentChange = (currentRow, oldCurrentRow) => emit('current-change', currentRow, oldCurrentRow)
const handleHeaderDragend = (newWidth, oldWidth, column, event) => emit('header-dragend', newWidth, oldWidth, column, event)
const handleExpandChange = (row, expandedRows) => emit('expand-change', row, expandedRows)
// 分页事件处理
const handleSizeChange = (size) => emit('size-change', size)
const handlePageChange = (page) => emit('page-change', page)
// 暴露表格方法
defineExpose({
tableRef,
clearSelection: () => tableRef.value?.clearSelection(),
toggleRowSelection: (row, selected) => tableRef.value?.toggleRowSelection(row, selected),
toggleAllSelection: () => tableRef.value?.toggleAllSelection(),
toggleRowExpansion: (row, expanded) => tableRef.value?.toggleRowExpansion(row, expanded),
setCurrentRow: (row) => tableRef.value?.setCurrentRow(row),
clearSort: () => tableRef.value?.clearSort(),
clearFilter: (columnKeys) => tableRef.value?.clearFilter(columnKeys),
doLayout: () => tableRef.value?.doLayout(),
sort: (prop, order) => tableRef.value?.sort(prop, order)
})
</script>
<style lang="scss" scoped>
.gva-table {
.gva-pagination {
margin-top: 20px;
text-align: right;
}
}
</style>
07.权限控制
a.权限指令
// src/directive/auth.js
import { useUserStore } from '@/pinia/modules/user'
// 权限检查函数
function checkPermission(el, binding) {
const { value } = binding
const userStore = useUserStore()
const roles = userStore.userInfo.authority?.defaultRouter || []
if (value && value instanceof Array && value.length > 0) {
const permissionRoles = value
const hasPermission = roles.some(role => {
return permissionRoles.includes(role)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error('权限指令需要传入数组参数')
}
}
// 权限指令
export default {
mounted(el, binding) {
checkPermission(el, binding)
},
updated(el, binding) {
checkPermission(el, binding)
}
}
b.按钮权限控制
// src/utils/btnAuth.js
import { useUserStore } from '@/pinia/modules/user'
// 检查按钮权限
export function checkBtnPermission(btnName) {
const userStore = useUserStore()
const btnAuth = userStore.userInfo.authority?.btns || []
return btnAuth.includes(btnName)
}
// 权限按钮组件
export function useBtnAuth() {
const userStore = useUserStore()
const hasAuth = (btnName) => {
const btnAuth = userStore.userInfo.authority?.btns || []
return btnAuth.includes(btnName)
}
return {
hasAuth
}
}
08.主题定制
a.Element Plus 主题定制
// src/style/element_variables.scss
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #409eff,
),
'success': (
'base': #67c23a,
),
'warning': (
'base': #e6a23c,
),
'danger': (
'base': #f56c6c,
),
'error': (
'base': #f56c6c,
),
'info': (
'base': #909399,
),
)
);
// 自定义组件样式
.el-button {
border-radius: 4px;
&.is-round {
border-radius: 20px;
}
}
.el-card {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.el-table {
.el-table__header {
th {
background-color: #fafafa;
color: #606266;
font-weight: 500;
}
}
}
b.暗色主题支持
// src/style/dark.scss
[data-theme='dark'] {
--el-bg-color: #141414;
--el-bg-color-page: #0a0a0a;
--el-bg-color-overlay: #1d1e1f;
--el-text-color-primary: #e5eaf3;
--el-text-color-regular: #cfd3dc;
--el-text-color-secondary: #a3a6ad;
--el-text-color-placeholder: #8d9095;
--el-text-color-disabled: #6c6e72;
--el-border-color: #4c4d4f;
--el-border-color-light: #414243;
--el-border-color-lighter: #363637;
--el-border-color-extra-light: #2b2b2c;
--el-border-color-dark: #58585b;
--el-border-color-darker: #636466;
--el-fill-color: #303133;
--el-fill-color-light: #262727;
--el-fill-color-lighter: #1d1d1d;
--el-fill-color-extra-light: #191919;
--el-fill-color-dark: #39393a;
--el-fill-color-darker: #424243;
--el-fill-color-blank: transparent;
// 自定义组件暗色样式
.layout-container {
background-color: var(--el-bg-color-page);
}
.gva-card {
background-color: var(--el-bg-color);
border-color: var(--el-border-color);
}
}
09.响应式设计
a.移动端适配
// src/style/mobile.scss
@media screen and (max-width: 768px) {
.layout-container {
.aside {
position: fixed;
top: 0;
left: -210px;
z-index: 1001;
transition: left 0.3s;
&.mobile-show {
left: 0;
}
}
.main-container {
margin-left: 0;
.navbar {
.hamburger-container {
display: block;
}
}
}
}
.gva-table {
.el-table {
font-size: 12px;
}
.gva-pagination {
.el-pagination {
justify-content: center;
.el-pagination__sizes,
.el-pagination__jump {
display: none;
}
}
}
}
.gva-form {
.el-form-item {
margin-bottom: 15px;
.el-form-item__label {
line-height: 20px;
margin-bottom: 5px;
}
}
}
}
@media screen and (max-width: 480px) {
.gva-search-box {
.el-form {
.el-form-item {
width: 100%;
margin-right: 0;
margin-bottom: 10px;
}
}
}
.gva-btn-list {
.el-button {
margin-bottom: 10px;
width: 100%;
}
}
}
10.性能优化
a.路由懒加载
// 路由懒加载配置
const routes = [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import(/* webpackChunkName: "dashboard" */ '@/view/dashboard/index.vue')
},
{
path: '/system',
name: 'System',
component: () => import(/* webpackChunkName: "system" */ '@/view/system/index.vue'),
children: [
{
path: 'user',
name: 'User',
component: () => import(/* webpackChunkName: "system-user" */ '@/view/system/user/index.vue')
}
]
}
]
b.组件懒加载
<template>
<div>
<!-- 使用 Suspense 包装异步组件 -->
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
// 异步组件
const AsyncComponent = defineAsyncComponent({
loader: () => import('@/components/heavy-component.vue'),
loadingComponent: () => import('@/components/loading.vue'),
errorComponent: () => import('@/components/error.vue'),
delay: 200,
timeout: 3000
})
</script>
c.图片懒加载
<template>
<div class="image-container">
<img
v-lazy="imageSrc"
:alt="imageAlt"
class="lazy-image"
@load="handleImageLoad"
@error="handleImageError"
>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
imageSrc: {
type: String,
required: true
},
imageAlt: {
type: String,
default: ''
}
})
const imageLoaded = ref(false)
const imageError = ref(false)
const handleImageLoad = () => {
imageLoaded.value = true
}
const handleImageError = () => {
imageError.value = true
}
</script>
<style scoped>
.image-container {
position: relative;
overflow: hidden;
}
.lazy-image {
width: 100%;
height: auto;
transition: opacity 0.3s;
}
.lazy-image[lazy=loading] {
opacity: 0.3;
}
.lazy-image[lazy=loaded] {
opacity: 1;
}
.lazy-image[lazy=error] {
opacity: 0.3;
}
</style>
11.测试
a.单元测试配置
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.js']
},
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
}
})
b.组件测试示例
// tests/components/GvaTable.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import GvaTable from '@/components/gva-table/index.vue'
import { ElTable, ElPagination } from 'element-plus'
describe('GvaTable', () => {
it('renders table with data', () => {
const tableData = [
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 }
]
const wrapper = mount(GvaTable, {
props: {
tableData,
total: 2,
currentPage: 1,
pageSize: 10
},
global: {
components: {
ElTable,
ElPagination
}
}
})
expect(wrapper.find('.gva-table').exists()).toBe(true)
expect(wrapper.find('.gva-pagination').exists()).toBe(true)
})
it('emits page-change event when page changes', async () => {
const wrapper = mount(GvaTable, {
props: {
tableData: [],
total: 100,
currentPage: 1,
pageSize: 10
}
})
await wrapper.vm.handlePageChange(2)
expect(wrapper.emitted('page-change')).toBeTruthy()
expect(wrapper.emitted('page-change')[0]).toEqual([2])
})
})
12.最佳实践
a.代码规范
使用 ESLint + Prettier 保证代码质量
遵循 Vue 3 Composition API 最佳实践
组件命名使用 PascalCase
文件命名使用 kebab-case
b.性能优化
合理使用 v-memo 和 v-once
避免在模板中使用复杂计算
使用 shallowRef 和 shallowReactive 优化响应式
合理拆分组件,避免组件过大
c.安全防护
对用户输入进行验证和过滤
使用 v-html 时注意 XSS 防护
敏感信息不要存储在前端
使用 HTTPS 传输数据
d.用户体验
提供加载状态提示
合理的错误处理和提示
响应式设计适配移动端
无障碍访问支持
13.常见问题
a.分类1
Q: 如何解决路由懒加载失败?
A: 检查路径是否正确,确保组件文件存在,可以添加错误处理。
b.分类2
Q: Element Plus 样式不生效?
A: 确保正确导入样式文件,检查 CSS 优先级和作用域。
c.分类3
Q: Pinia 状态丢失?
A: 检查是否正确持久化状态,页面刷新时重新初始化状态。
d.分类4
Q: 打包后静态资源路径错误?
A: 检查 Vite 配置中的 base 路径和 publicPath 设置。
3.2 环境变量
01.env环境变量
a.env.development
ENV = 'development' // 标识 不要管
VITE_CLI_PORT = 8080
VITE_SERVER_PORT = 8888
VITE_BASE_API = /api
VITE_FILE_API = /api
VITE_BASE_PATH = http://127.0.0.1
VITE_POSITION = close // open为开启代码定位功能,close为关闭代码定位功能
VITE_EDITOR = vscode // 可选 vscode webstorm
// VITE_EDITOR = webstorm 如果使用webstorm开发且要使用dom定位到代码行功能 请先自定添加 webstorm到环境变量 再将VITE_EDITOR值修改为webstorm
// 如果使用docker-compose开发模式,设置为下面的地址或本机主机IP
// VITE_BASE_PATH = http://177.7.0.12
// 打开代码定位功能的情况下,在web页面按住键盘的shift+alt+鼠标左键点击代码行,即可在编辑器中打开对应的代码文件
b.env.production
ENV = 'production' // 标识 不要管
#下方为上线需要用到的程序代理前缀,一般用于nginx代理转发
VITE_BASE_API = /api
VITE_FILE_API = /api
#下方修改为你的线上ip(如果需要在线使用表单构建工具时使用,其余情况无需使用以下环境变量)
VITE_BASE_PATH = https://demo.gin-vue-admin.com
3.3 按钮权限
01.按钮权限
a.说明
实现方式是采用 vue 原生的注册指令方式
b.实现代码
// 权限按钮展示指令
import { useUserStore } from '@/pinia/modules/user'
export default {
install: (app) => {
const userStore = useUserStore()
app.directive('auth', {
// 当被绑定的元素插入到 DOM 中时……
mounted: function(el, binding) {
const userInfo = userStore.userInfo
let type = ''
switch (Object.prototype.toString.call(binding.value)) {
case '[object Array]':
type = 'Array'
break
case '[object String]':
type = 'String'
break
case '[object Number]':
type = 'Number'
break
default:
type = ''
break
}
if (type === '') {
el.parentNode.removeChild(el)
return
}
const waitUse = binding.value.toString().split(',')
let flag = waitUse.some(item => item === userInfo.authorityId)
if (binding.modifiers.not) {
flag = !flag
}
if (!flag) {
el.parentNode.removeChild(el)
}
}
})
}
}
02.创建按钮
a.分类1
进入菜单管理界面,点击新增或编辑,点击下方的新增可控按钮填入按钮名称(英文且不重复)和描述后点击确定
b.分类2
进入权限管理,点击设置权限-->分配菜单,如果此菜单有可控按钮,则会显示分配按钮权限字样,点击进行分配
c.分类3
点击确定,完成分配。
03.代码操作
a.说明
进入拥有可被控按钮菜单对应的前端页面,添加引入按钮控制组件,并在template中添加v-auth指令赋予需要被控制的按钮名称
b.代码
<template>
<div>
// btnAuth为权限组件固定写法 .a a为菜单管理中创建的按钮名称 配置过后即可实现对按钮的控制
<div v-auth="btnAuth.a">按钮1</div>
<div v-auth="btnAuth.b">按钮2</div>
<div v-auth="btnAuth.c">按钮3</div>
</div>
</template>
<script setup>
import { useBtnAuth } from '@/utils/btnAuth'
const btnAuth = useBtnAuth()
</script>
3.4 字典方法
01.字典功能
a.说明
字典是在后端数据库中存储的kv对,通过字典的value找到对应的文本展示内容 字典管理中进行录入
b.说明
具体录入视频参考 https://www.bilibili.com/video/BV1kv4y1g7nT?p=12&vd_source=f2640257c21e3b547a790461ed94875e
02.字典方法
import { getDict } from '@/utils/dictionary'
// getDict 方法 可以根据在字典录入的字典的type找到对应的字典内容数组 从而在前端可以实现系列使用字典的操作
// 例如:
const sexDict = await getDict("sex")
// res即返回的字典数组
// [{value:0,lable:"男"},{value:1,lable:"女"}]
import { showDictLabel } from '@/utils/dictionary'
// showDictLabel 方法 可以根据在字典录入的字典和传入的value值匹配出对应的字典label
// 例如:
const label = showDictLabel(sexDict,0)
// label即返回的内容
// 男
// 或者在html部分直接使用 {{showDictLabel(sexDict,0)}} 获得文字 男
3.5 自定义全局皮肤
01.自定义样式
a.说明
v2.6.4版本支持通过config.json进行页面配置
配置文件为 /web/src/config.json
b.代码
{
"weakness":false,
"grey":false,
"primaryColor":"#3b82f6",
"showTabs":true,
"darkMode":"auto",
"layout_side_width":200,
"layout_side_collapsed_width":60,
"layout_side_item_height":44,
"show_watermark":true
}
c.说明
本json文件可以通过页面中可视化调整获得
3.6 自定义图标
01.自定义图标【菜单以及直接使用】
a.说明
在2023/12/10后下载的版本即可使用,之前的版本请自行升级
b.使用
我们以文件 web/assets/icons/customer-gva.svg 为例,
只需要把svg文件放入前端 web/assets/icons/ 目录下即可自动注册,然后在菜单配置中选择使用或者直接使用标签<customer-gva></customer-gva>即可使用
svg文件不可以有宽和高,如果需要自动适配颜色,需要在svg文件中添加fill="currentColor"属性,如果使用特定颜色svg自行填充fill即可
c.实现代码:/web/core/global.js
const createIconComponent = (svgContent) => ({
name: 'SvgIcon',
props: {
iconClass: {
type: String,
default: '',
},
className: {
type: String,
default: '',
},
},
render() {
const { className } = this
return h('span', {
class: className,
ariaHidden: true,
innerHTML: svgContent,
})
},
})
const registerIcons = async(app) => {
const iconModules = import.meta.glob('@/assets/icons/**/*.svg')
for (const path in iconModules) {
const response = await fetch(path)
const svgContent = await response.text()
const iconName = path.split('/').pop().replace(/\.svg$/, '')
const iconComponent = createIconComponent(svgContent)
config.logs.push({
'key': iconName,
'label': iconName,
})
app.component(iconName, iconComponent)
}
}
3.7 开启TypeScript
01.开启TypeScript
a.说明
在创建.vue文件的时候,.vue内部的所有<script>标签,添加lang="ts"属性,
例:<script lang="ts"> <script lang="ts" setup> 即可在当前的vue组件中开启ts模式。您可以畅快得使用您需要的ts语法。
b.说明
如果您其他组件不需要ts,那么按照原始的不携带lang=ts的标签即可继续使用js进行开发,
项目支持不同组件使用不同的语言进行同步开发。您可以根据您的喜好和需求做出最佳选择。
3.8 导出Excel
01.名词解释
a.分类
业务库:注:需要提前到db-list自行配置多数据库,如未配置需配置后重启服务方可使用。若无法选择,请到config.yaml中设置disabled:false,选择导入导出的目标库。
模板名称: 模板的中文标记,主要用来用户助记。
表名称: 需要导出数据的表,输入表名即可。
模板标识: 模板的唯一标识,用来区分不同的模板。在使用导出组件时候作为 templateId 参数传入。
关联条件: 此处可以添加多个关联,需要选择和写入的参数为,join方式[inner, left, right],关联表,关联条件。
b.模板信息: 此处接收一个json string,用来配置导出的表头,格式如下:
{
"表的列": "导出的中文名称列",
}
c.示例(xxx.xxx标识带json模式)
{
"id": "ID",
"name": "名称",
"age": "年龄",
"info.id": "用户信息组ID"
}
d.总结
默认导出条数: 默认导出的条数,如果不填写则默认为全量导出,实际使用中以ExportExcel的入参limit为准。
默认排序条件: 默认的排序条件,如果不填写则默认为无排序,实际使用中以ExportExcel的入参order为准。
导出条件:此处可以添加多条,每条需要填入的信息为 json中输入的key, 对应表的column, 条件。
02.组件使用
a.在你需要导出的页面<script>标签添加下方的组件
// 导出组件
import ExportExcel from '@/components/exportExcel/exportExcel.vue'
// 导入组件
import ImportExcel from '@/components/exportExcel/importExcel.vue'
// 导出模板组件
import ExportTemplate from '@/components/exportExcel/exportTemplate.vue'
b.然后在<template>中使用即可
<!-- 导出组件-->
<ExportExcel templateId="api" :condition="你的查询条件对象" :limit="10" :offset="10" order="id desc"/>
<!-- 导入组件 handleSuccess为导入成功后的回调函数-->
<ImportExcel templateId="api" @on-success="handleSuccess"/>
<!-- 导出模板-->
<ExportTemplate templateId="api" />
c.入参解释
condition: { // 可以传入查询条件 根据模板中配置的查询条件映射关系进行有条件导出
type: Object,
default: () => ({})
}
limit: { // 可以限制条目 根据模板中可以配置默认的条目限制 此处入参的优先级高于模板中的配置
type: Number,
default: 0
}
offset: { // 可以限制偏移量 根据模板中可以配置默认的偏移量 此处入参的优先级高于模板中的配置
type: Number,
default: 0
}
order: { // 可以限制排序 根据模板中可以配置默认的排序 此处入参的优先级高于模板中的配置
type: String,
default: ''
}
onSuccess: { // 导入成功后的回调函数
type: Function,
default: () => {}
}
此标签会产生一个按钮,点击即可导出对应表,后续会更新导出条件,敬请期待。
4 后端管理
4.1 操作指南
01.server项目结构
├── api
│ └── v1
├── config
├── core
├── docs
├── global
├── initialize
│ └── internal
├── middleware
├── model
│ ├── request
│ └── response
├── packfile
├── resource
│ ├── excel
│ ├── page
│ └── template
├── router
├── service
├── source
└── utils
├── timer
└── upload
02.说明
文件夹 说明 描述
api api层 api层
--v1 v1版本接口 v1版本接口
config 配置包 config.yaml对应的配置结构体
core 核心文件 核心组件(zap, viper, server)的初始化
docs swagger文档目录 swagger文档目录
global 全局对象 全局对象
initialize 初始化 router,redis,gorm,validator, timer的初始化
--internal 初始化内部函数 gorm 的 longger 自定义,在此文件夹的函数只能由 initialize 层进行调用
middleware 中间件层 用于存放 gin 中间件代码
model 模型层 模型对应数据表
--request 入参结构体 接收前端发送到后端的数据。
--response 出参结构体 返回给前端的数据结构体
packfile 静态文件打包 静态文件打包
resource 静态资源文件夹 负责存放静态文件
--excel excel导入导出默认路径 excel导入导出默认路径
--page 表单生成器 表单生成器 打包后的dist
--template 模板 模板文件夹,存放的是代码生成器的模板
router 路由层 路由层
service service层 存放业务逻辑问题
source source层 存放初始化数据的函数
utils 工具包 工具函数封装
--timer timer 定时器接口封装
--upload oss oss接口封装
4.2 配置文件
01.JWT
a.yaml配置
# jwt configuration
jwt:
signing-key: 'qmPlus'
expires-time: 7d
buffer-time: 1d
b.struct定义
type JWT struct {
SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` // jwt签名
ExpiresTime string `mapstructure:"expires-time" json:"expires-time" yaml:"expires-time"` // 过期时间
BufferTime string `mapstructure:"buffer-time" json:"buffer-time" yaml:"buffer-time"` // 缓冲时间
Issuer string `mapstructure:"issuer" json:"issuer" yaml:"issuer"` // 签发者
}
c.描述
配置名 类型 说明
signing-key string jwt的签名
expires-time int64 过期时间
buffer-time int64 缓冲时间(过期前这段时间内有过请求会刷新jwt续期)
issuer string jwt签发者
02.Zap
a.yaml配置
# zap logger configuration
zap:
level: 'info'
format: 'console'
prefix: '[GIN-VUE-ADMIN]'
director: 'log'
show-line: true
encode-level: 'LowercaseColorLevelEncoder'
stacktrace-key: 'stacktrace'
log-in-console: true
retention-day: 7
b.struct定义
type Zap struct {
Level string `mapstructure:"level" json:"level" yaml:"level"` // 级别
Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 日志前缀
Format string `mapstructure:"format" json:"format" yaml:"format"` // 输出
Director string `mapstructure:"director" json:"director" yaml:"director"` // 日志文件夹
EncodeLevel string `mapstructure:"encode-level" json:"encode-level" yaml:"encode-level"` // 编码级
StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktrace-key" yaml:"stacktrace-key"` // 栈名
ShowLine bool `mapstructure:"show-line" json:"show-line" yaml:"show-line"` // 显示行
LogInConsole bool `mapstructure:"log-in-console" json:"log-in-console" yaml:"log-in-console"` // 输出控制台
RetentionDay int `mapstructure:"retention-day" json:"retention-day" yaml:"retention-day"` // 日志保留天数
}
c.描述
配置名 类型 说明
level string level的模式的详细说明,请看zap官方文档
format string console: 控制台形式输出日志 json: json格式输出日志
prefix string 日志的前缀
director string 存放日志的文件夹,修改即可,不需要手动创建
show_line bool 显示行号, 默认为true,不建议修改
encode_level string LowercaseLevelEncoder:小写
stacktrace_key string 堆栈的名称,即在json格式输出日志时的josn的key
log_in_console bool 是否输出到控制台,默认为true
retention_day int 日志保留天数
03.Redis
a.yaml配置
# redis configuration
redis:
name: ''
addr: '127.0.0.1:6379'
password: ''
db: 0
use-cluster: false
cluster-addrs: []
b.struct定义
type Redis struct {
Name string `mapstructure:"name" json:"name" yaml:"name"` // 代表当前实例的名字
Addr string `mapstructure:"addr" json:"addr" yaml:"addr"` // 服务器地址:端口
Password string `mapstructure:"password" json:"password" yaml:"password"` // 密码
DB int `mapstructure:"db" json:"db" yaml:"db"` // 单实例模式下redis的哪个数据库
UseCluster bool `mapstructure:"useCluster" json:"useCluster" yaml:"useCluster"` // 是否使用集群模式
ClusterAddrs []string `mapstructure:"clusterAddrs" json:"clusterAddrs" yaml:"clusterAddrs"` // 集群模式下的节点地址列表
}
c.描述
配置名 类型 说明
name string 代表当前实例的名字
addr string redis连接地址及端口
password string 密码
db int redis的哪个数据库
use-cluster bool 是否使用集群模式
cluster-addrs []string 集群模式下的节点地址列表
04.Email
a.yaml配置
# email configuration
email:
to: '[email protected]'
port: 465
from: '[email protected]'
host: 'smtp.163.com'
is-ssl: true
secret: 'xxx'
nickname: 'test'
is-loginauth: false
b.struct定义
type Email struct {
To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:[email protected] [email protected] 正式开发中请把此项目作为参数使用
From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱
Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议
Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥
Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱
Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465
IsSSL bool `mapstructure:"is-ssl" json:"is-ssl" yaml:"is-ssl"` // 是否SSL 是否开启SSL
IsLoginAuth bool `mapstructure:"is-loginauth" json:"is-loginauth" yaml:"is-loginauth"` // 是否LoginAuth 是否使用LoginAuth认证方式(适用于IBM、微软邮箱服务器等)
}
c.描述
配置名 类型 说明
to string 邮件接收者,可以是多个,以英文逗号(,)进行区分,最好别带空格,如果是一个邮箱最后请不要加英文逗号(,)
from string 发件人邮箱
host string 邮箱的主服务器地址
secret string 密钥,用于登录的密钥,最好不要用邮箱密码
nickname string 发件人昵称
port int 邮件服务端口
is-ssl bool 是否使用SSL
is-loginauth bool 是否使用LoginAuth认证方式(适用于IBM、微软邮箱服务器等)
05.Casbin
a.yaml配置
# casbin configuration
casbin:
model-path: './resource/rbac_model.conf'
b.struct定义
type Casbin struct {
ModelPath string `mapstructure:"model-path" json:"modelPath" yaml:"model-path"`
}
c.描述
配置名 类型 说明 建议是否修改
model-path string 存放casbin模型的相对路径,默认值为./resource/rbac_model.conf 不推荐修改
06.System
a.yaml配置
# system configuration
system:
env: 'public'
db-type: 'mysql'
oss-type: 'local'
router-prefix: ''
addr: 8888
iplimit-count: 15000
iplimit-time: 3600
use-multipoint: false
use-redis: false
use-mongo: false
use-strict-auth: false
disabled-auto-migrate: false
b.struct定义
type System struct {
Env string `mapstructure:"env" json:"env" yaml:"env"` // 环境值
DbType string `mapstructure:"db-type" json:"db-type" yaml:"db-type"` // 数据库类型:mysql(默认)|sqlite|sqlserver|postgresql
OssType string `mapstructure:"oss-type" json:"oss-type" yaml:"oss-type"` // Oss类型
RouterPrefix string `mapstructure:"router-prefix" json:"router-prefix" yaml:"router-prefix"` // 路由前缀
Addr int `mapstructure:"addr" json:"addr" yaml:"addr"` // 端口值
LimitCountIP int `mapstructure:"iplimit-count" json:"iplimit-count" yaml:"iplimit-count"` // 限制同IP访问次数
LimitTimeIP int `mapstructure:"iplimit-time" json:"iplimit-time" yaml:"iplimit-time"` // 限制时间
UseMultipoint bool `mapstructure:"use-multipoint" json:"use-multipoint" yaml:"use-multipoint"` // 多点登录拦截
UseRedis bool `mapstructure:"use-redis" json:"use-redis" yaml:"use-redis"` // 使用redis
UseMongo bool `mapstructure:"use-mongo" json:"use-mongo" yaml:"use-mongo"` // 使用mongo
UseStrictAuth bool `mapstructure:"use-strict-auth" json:"use-strict-auth" yaml:"use-strict-auth"` // 使用树形角色分配模式
DisabledAutoMigrate bool `mapstructure:"disabled-auto-migrate" json:"disabled-auto-migrate" yaml:"disabled-auto-migrate"` // 是否关闭自动迁移数据库
}
c.描述
配置名 类型 说明
env string 环境模式,"develop"为开发模式(跳过身份验证),"public"为生产模式
addr int 后端服务端口,默认8888
db-type string 数据库类型,支持:mysql、pgsql、sqlite、mssql、oracle
oss-type string 对象存储类型:local(本地存储)、qiniu(七牛云)、aliyun(阿里云)、minio
router-prefix string 路由前缀,用于API路由统一前缀
use-multipoint bool 是否启用多点登录拦截(单点登录),默认false
use-redis bool 是否使用Redis缓存,默认false
iplimit-count int IP限流:指定时间段内同IP最大访问次数,默认15000
iplimit-time int IP限流:限制时间窗口(秒),默认3600
use-mongo bool 是否使用MongoDB数据库,默认false
use-strict-auth bool 是否开启严格角色模式(树形角色分配),默认false
disabled-auto-migrate bool 是否自动迁移数据库,默认false,生产环境建议开启
07.Captcha
a.yaml配置
# captcha configuration
captcha:
key-long: 6
img-width: 240
img-height: 80
open-captcha: 0
open-captcha-timeout: 3600
b.struct定义
type Captcha struct {
KeyLong int `mapstructure:"key-long" json:"key-long" yaml:"key-long"` // 验证码长度
ImgWidth int `mapstructure:"img-width" json:"img-width" yaml:"img-width"` // 验证码宽度
ImgHeight int `mapstructure:"img-height" json:"img-height" yaml:"img-height"` // 验证码高度
OpenCaptcha int `mapstructure:"open-captcha" json:"open-captcha" yaml:"open-captcha"` // 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码次数,如3代表错误三次后出现验证码
OpenCaptchaTimeOut int `mapstructure:"open-captcha-timeout" json:"open-captcha-timeout" yaml:"open-captcha-timeout"` // 防爆破验证码超时时间,单位:s(秒)
}
c.描述
配置名 类型 说明
key-long int 验证码长度
img-width int 验证码宽度
img-height int 验证码高度
open-captcha int 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码次数
open-captcha-timeout int 防爆破验证码超时时间,单位:s(秒)
08.Mysql [pgsql,sqlite,mssql,oracle]
a.yaml配置
# mysql connect configuration
mysql:
path: ''
port: ''
config: ''
db-name: ''
username: ''
password: ''
prefix: ''
singular: false
engine: 'InnoDB'
max-idle-conns: 10
max-open-conns: 100
log-mode: 'info'
log-zap: false
b.struct定义
type Mysql struct {
GeneralDB `yaml:",inline" mapstructure:",squash"`
}
type GeneralDB struct {
Path string `mapstructure:"path" json:"path" yaml:"path"`
Port string `mapstructure:"port" json:"port" yaml:"port"`
Config string `mapstructure:"config" json:"config" yaml:"config"`
Dbname string `mapstructure:"db-name" json:"db-name" yaml:"db-name"`
Username string `mapstructure:"username" json:"username" yaml:"username"`
Password string `mapstructure:"password" json:"password" yaml:"password"`
Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"`
Singular bool `mapstructure:"singular" json:"singular" yaml:"singular"`
Engine string `mapstructure:"engine" json:"engine" yaml:"engine" default:"InnoDB"`
MaxIdleConns int `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns"`
MaxOpenConns int `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns"`
LogMode string `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode"`
LogZap bool `mapstructure:"log-zap" json:"log-zap" yaml:"log-zap"`
}
c.描述
配置名 类型 说明
path string 数据库服务器地址
port string 数据库端口
username string 数据库用户名
password string 数据库密码
db-name string 数据库名
config string 数据库连接高级配置
prefix string 表名前缀
singular bool 是否使用单数表名
engine string 数据库引擎,默认InnoDB
max-idle-conns int 设置空闲中的最大连接数
max-open-conns int 设置打开到数据库的最大连接数
log-mode string 开启Gorm全局日志等级:"silent"、"error"、"warn"、"info",默认info
log-zap bool 是否通过zap写入日志文件
09.Local
a.yaml配置
# local configuration
local:
path: 'uploads/file'
store-path: 'uploads/file'
b.struct定义
type Local struct {
Path string `mapstructure:"path" json:"path" yaml:"path"` // 本地文件访问路径
StorePath string `mapstructure:"store-path" json:"store-path" yaml:"store-path"` // 本地文件存储路径
}
c.描述
配置名 类型 说明
path string 本地文件访问路径
store-path string 本地文件存储路径
10.Qiniu
a.yaml配置
# qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址)
qiniu:
zone: '你的空间区域'
bucket: '你的空间名'
img-path: '你的oss域名'
use-https: false
access-key: 'xxxxxxxxxxxxxxxxxxxxxxxxx'
secret-key: 'xxxxxxxxxxxxxxxxxxxxxxxxx'
use-cdn-domains: false
b.struct定义
type Qiniu struct {
Zone string `mapstructure:"zone" json:"zone" yaml:"zone"`
Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"`
ImgPath string `mapstructure:"img-path" json:"imgPath" yaml:"img-path"`
UseHTTPS bool `mapstructure:"use-https" json:"useHttps" yaml:"use-https"`
AccessKey string `mapstructure:"access-key" json:"accessKey" yaml:"access-key"`
SecretKey string `mapstructure:"secret-key" json:"secretKey" yaml:"secret-key"`
UseCdnDomains bool `mapstructure:"use-cdn-domains" json:"useCdnDomains" yaml:"use-cdn-domains"`
}
c.描述
配置名 类型 说明
zone string 存储区域 Zone ,可配置选项为 ZoneHuadong / ZoneHuabei / ZoneHuanan / ZoneBeimei / ZoneXinjiapo
bucket string 存储空间
img-path string CDN 加速域名
use-https bool 是否使用https
access-key string 秘钥AK
secret-key string 秘钥SK
use-cdn-domains bool 上传是否使用CDN上传加速
11.AutoCode
a.yaml配置
# autocode configuration
autocode:
web: '/web/src'
root: ''
server: '/server'
module: 'github.com/flipped-aurora/gin-vue-admin/server'
ai-path: ''
b.struct定义
type Autocode struct {
Web string `mapstructure:"web" json:"web" yaml:"web"`
Root string `mapstructure:"root" json:"root" yaml:"root"`
Server string `mapstructure:"server" json:"server" yaml:"server"`
Module string `mapstructure:"module" json:"module" yaml:"module"`
AiPath string `mapstructure:"ai-path" json:"ai-path" yaml:"ai-path"`
}
c.描述
配置名 类型 说明
web string 前端项目路径
root string 项目根目录,自动适配,请不要手动配置
server string 服务端项目路径
module string Go模块名称
ai-path string AI相关路径
4.3 认证系统
01.认证机制概述
a.JWT认证流程
sequenceDiagram
participant C as 客户端
participant S as 服务器
participant DB as 数据库
C->>S: 1. 登录请求 (用户名/密码)
S->>DB: 2. 验证用户凭据
DB-->>S: 3. 返回用户信息
S->>S: 4. 生成 JWT Token
S-->>C: 5. 返回 Token
Note over C: 客户端存储 Token
C->>S: 6. API 请求 + Authorization Header
S->>S: 7. 验证 Token
S-->>C: 8. 返回数据或拒绝访问
02.JWT配置
a.配置文件设置
jwt:
signing-key: 'qmPlus' # JWT 签名密钥
expires-time: 604800s # Token 过期时间 (7天)
buffer-time: 86400s # Token 缓冲时间 (1天)
issuer: 'qmPlus' # 签发者
b.配置参数说明
参数 类型 说明 默认值
signing-key string JWT 签名密钥,用于生成和验证 Token qmPlus
expires-time duration Token 有效期,过期后需要重新登录 604800s (7天)
buffer-time duration Token 缓冲时间,在此时间内可以刷新 Token 86400s (1天)
issuer string Token 签发者标识 qmPlus
03.核心组件
a.JWT中间件,server/middleware/jwt.go
// JWTAuth JWT认证中间件
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 从请求头获取 Token
token := c.Request.Header.Get("x-token")
if token == "" {
response.FailWithDetailed(gin.H{"reload": true}, "未登录或非法访问", c)
c.Abort()
return
}
// 验证 Token
j := utils.NewJWT()
claims, err := j.ParseToken(token)
if err != nil {
if err == utils.TokenExpired {
response.FailWithDetailed(gin.H{"reload": true}, "授权已过期", c)
c.Abort()
return
}
response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c)
c.Abort()
return
}
// 将用户信息存储到上下文
c.Set("claims", claims)
c.Next()
}
}
b.JWT工具类,server/utils/jwt.go
type JWT struct {
SigningKey []byte
}
type CustomClaims struct {
BaseClaims
BufferTime int64
jwt.StandardClaims
}
type BaseClaims struct {
UUID uuid.UUID
ID uint
Username string
NickName string
AuthorityId string
}
// CreateToken 创建Token
func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey)
}
// ParseToken 解析Token
func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if err != nil {
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, TokenMalformed
} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
return nil, TokenExpired
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, TokenNotValidYet
} else {
return nil, TokenInvalid
}
}
}
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, TokenInvalid
}
04.登录实现
a.登录API,server/api/v1/sys_user.go
// Login 用户登录
func (b *BaseApi) Login(c *gin.Context) {
var l systemReq.Login
err := c.ShouldBindJSON(&l)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
// 验证码校验
if store.Verify(l.CaptchaId, l.Captcha, true) {
// 验证用户凭据
u := &system.SysUser{Username: l.Username, Password: l.Password}
user, err := userService.Login(u)
if err != nil {
global.GVA_LOG.Error("登陆失败! 用户名不存在或者密码错误!", zap.Error(err))
response.FailWithMessage("用户名不存在或者密码错误", c)
return
}
// 生成 JWT Token
b.tokenNext(c, *user)
} else {
response.FailWithMessage("验证码错误", c)
}
}
// tokenNext 生成Token并返回
func (b *BaseApi) tokenNext(c *gin.Context, user system.SysUser) {
j := &utils.JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)}
claims := j.CreateClaims(utils.BaseClaims{
UUID: user.UUID,
ID: user.ID,
NickName: user.NickName,
Username: user.Username,
AuthorityId: user.AuthorityId,
})
token, err := j.CreateToken(claims)
if err != nil {
global.GVA_LOG.Error("获取token失败!", zap.Error(err))
response.FailWithMessage("获取token失败", c)
return
}
// 多点登录控制
if !global.GVA_CONFIG.System.UseMultipoint {
response.OkWithDetailed(systemRes.LoginResponse{
User: user,
Token: token,
ExpiresAt: claims.StandardClaims.ExpiresAt * 1000,
}, "登录成功", c)
return
}
// 单点登录处理
if jwtStr, err := jwtService.GetRedisJWT(user.Username); err == redis.Nil {
if err := jwtService.SetRedisJWT(token, user.Username); err != nil {
global.GVA_LOG.Error("设置登录状态失败!", zap.Error(err))
response.FailWithMessage("设置登录状态失败", c)
return
}
response.OkWithDetailed(systemRes.LoginResponse{
User: user,
Token: token,
ExpiresAt: claims.StandardClaims.ExpiresAt * 1000,
}, "登录成功", c)
} else if err != nil {
global.GVA_LOG.Error("设置登录状态失败!", zap.Error(err))
response.FailWithMessage("设置登录状态失败", c)
} else {
var blackJWT system.JwtBlacklist
blackJWT.Jwt = jwtStr
if err := jwtService.JsonInBlacklist(blackJWT); err != nil {
response.FailWithMessage("jwt作废失败", c)
return
}
if err := jwtService.SetRedisJWT(token, user.Username); err != nil {
response.FailWithMessage("设置登录状态失败", c)
return
}
response.OkWithDetailed(systemRes.LoginResponse{
User: user,
Token: token,
ExpiresAt: claims.StandardClaims.ExpiresAt * 1000,
}, "登录成功", c)
}
}
05.Token刷新机制
a.自动刷新,当 Token 即将过期时(在 buffer-time 时间内),系统会自动刷新 Token
// RefreshToken 刷新Token
func (j *JWT) RefreshToken(tokenString string) (string, error) {
jwt.TimeFunc = func() time.Time {
return time.Unix(0, 0)
}
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
jwt.TimeFunc = time.Now
claims.StandardClaims.ExpiresAt = time.Now().Add(time.Duration(global.GVA_CONFIG.JWT.ExpiresTime) * time.Second).Unix()
return j.CreateToken(*claims)
}
return "", TokenInvalid
}
06.Token黑名单
a.黑名单机制,为了支持用户登出和 Token 撤销,系统实现了 JWT 黑名单机制
// JwtBlacklist JWT黑名单结构体
type JwtBlacklist struct {
global.GVA_MODEL
Jwt string `gorm:"type:text;comment:jwt"`
}
// JsonInBlacklist 拉黑jwt
func (jwtService *JwtService) JsonInBlacklist(jwtList system.JwtBlacklist) (err error) {
err = global.GVA_DB.Create(&jwtList).Error
if err != nil {
return
}
global.BlackCache.SetDefault(jwtList.Jwt, struct{}{})
return
}
// IsBlacklist 判断JWT是否在黑名单内部
func (jwtService *JwtService) IsBlacklist(jwt string) bool {
_, ok := global.BlackCache.Get(jwt)
return ok
}
07.安全最佳实践
a.密钥管理
使用强随机密钥作为签名密钥
定期轮换签名密钥
将密钥存储在安全的配置文件中
b.Token生命周期
设置合理的过期时间(建议不超过24小时)
实现 Token 刷新机制
支持主动撤销 Token
c.传输安全
始终使用 HTTPS 传输 Token
在请求头中传递 Token,避免在 URL 中暴露
客户端安全存储 Token
d.多点登录控制
system:
use-multipoint: true # 启用单点登录限制
08.常见问题
a.Token过期如何处理?
系统会返回特定的错误码,前端应该引导用户重新登录或自动刷新 Token。
b.如何实现记住登录状态?
可以设置较长的 Token 过期时间,或者实现 Refresh Token 机制。
c.多设备登录如何控制?
通过配置 use-multipoint: true 启用单点登录,或者实现设备管理功能。
d.JWT密钥泄露怎么办?
立即更换密钥,使所有现有 Token 失效,要求用户重新登录。
4.4 权限系统
00.汇总
Gin-Vue-Admin 采用 Casbin 实现基于角色的访问控制 (RBAC),提供灵活、强大的权限管理机制,支持多层级权限控制。
01.权限模型概述
a.RBAC权限模型
用户 (User) ──┐
├─→ 角色 (Role) ──→ 权限 (Permission) ──→ 资源 (Resource)
用户组 (Group) ┘
b.权限层级结构
graph TD
A[超级管理员] --> B[系统管理员]
A --> C[业务管理员]
B --> D[普通用户]
C --> D
B --> E[用户管理权限]
B --> F[系统配置权限]
C --> G[业务数据权限]
D --> H[基础查看权限]
E --> I[API: /user/*]
F --> J[API: /system/*]
G --> K[API: /business/*]
H --> L[API: /base/*]
02.Casbin配置
a.模型配置文件,server/resource/rbac_model.conf
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
b.配置参数说明
配置项 说明
request_definition 请求定义:主体(sub)、对象(obj)、动作(act)
policy_definition 策略定义:权限规则格式
role_definition 角色定义:角色继承关系
policy_effect 策略效果:允许访问的条件
matchers 匹配器:权限验证逻辑
03.核心组件
a.Casbin中间件,server/middleware/casbin_rbac.go
// CasbinHandler Casbin权限验证中间件
func CasbinHandler() gin.HandlerFunc {
return func(c *gin.Context) {
claims, _ := c.Get("claims")
waitUse := claims.(*utils.CustomClaims)
// 获取请求信息
obj := c.Request.URL.Path
act := c.Request.Method
sub := waitUse.AuthorityId
// Casbin权限验证
e := casbinService.Casbin()
success, _ := e.Enforce(sub, obj, act)
if !success {
response.FailWithDetailed(gin.H{}, "权限不足", c)
c.Abort()
return
}
c.Next()
}
}
b.Casbin服务,server/service/sys_casbin.go
type CasbinService struct{}
// UpdateCasbin 更新Casbin权限
func (casbinService *CasbinService) UpdateCasbin(authorityId string, casbinInfos []request.CasbinInfo) error {
casbinService.ClearCasbin(0, authorityId)
rules := [][]string{}
for _, v := range casbinInfos {
rules = append(rules, []string{authorityId, v.Path, v.Method})
}
e := casbinService.Casbin()
success, _ := e.AddPolicies(rules)
if !success {
return errors.New("存在相同api,添加失败,请联系管理员")
}
return nil
}
// GetPolicyPathByAuthorityId 获取权限列表
func (casbinService *CasbinService) GetPolicyPathByAuthorityId(authorityId string) (pathMaps []request.CasbinInfo) {
e := casbinService.Casbin()
list := e.GetFilteredPolicy(0, authorityId)
for _, v := range list {
pathMaps = append(pathMaps, request.CasbinInfo{
Path: v[1],
Method: v[2],
})
}
return pathMaps
}
// ClearCasbin 清除权限
func (casbinService *CasbinService) ClearCasbin(v int, p ...string) bool {
e := casbinService.Casbin()
success, _ := e.RemoveFilteredPolicy(v, p...)
return success
}
// Casbin 获取Casbin实例
func (casbinService *CasbinService) Casbin() *casbin.Enforcer {
return global.GVA_CASBIN
}
04.权限数据结构
a.角色表 (sys_authorities)
type SysAuthority struct {
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `sql:"index"`
AuthorityId string `json:"authorityId" gorm:"not null;unique;primary_key;comment:角色ID;size:90"`
AuthorityName string `json:"authorityName" gorm:"comment:角色名"`
ParentId string `json:"parentId" gorm:"comment:父角色ID"`
DataAuthorityId []string `json:"dataAuthorityId" gorm:"-"`
Children []SysAuthority `json:"children" gorm:"-"`
SysBaseMenus []SysBaseMenu `json:"menus" gorm:"many2many:sys_authority_menus;"`
Users []SysUser `json:"-" gorm:"many2many:sys_user_authority;"`
DefaultRouter string `json:"defaultRouter" gorm:"comment:默认菜单;default:dashboard"`
}
b.权限规则表 (casbin_rule)
type CasbinRule struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Ptype string `gorm:"size:512;uniqueIndex:unique_index"`
V0 string `gorm:"size:512;uniqueIndex:unique_index"`
V1 string `gorm:"size:512;uniqueIndex:unique_index"`
V2 string `gorm:"size:512;uniqueIndex:unique_index"`
V3 string `gorm:"size:512;uniqueIndex:unique_index"`
V4 string `gorm:"size:512;uniqueIndex:unique_index"`
V5 string `gorm:"size:512;uniqueIndex:unique_index"`
}
c.API权限表 (sys_apis)
type SysApi struct {
global.GVA_MODEL
Path string `json:"path" gorm:"comment:api路径"`
Description string `json:"description" gorm:"comment:api中文描述"`
ApiGroup string `json:"apiGroup" gorm:"comment:api组"`
Method string `json:"method" gorm:"default:POST;comment:方法"`
}
05.权限管理功能
a.角色管理
a.创建角色
// CreateAuthority 创建角色
func (authorityService *AuthorityService) CreateAuthority(auth system.SysAuthority) (authority system.SysAuthority, err error) {
var authorityBox system.SysAuthority
if !errors.Is(global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&authorityBox).Error, gorm.ErrRecordNotFound) {
return auth, errors.New("存在相同角色id")
}
err = global.GVA_DB.Create(&auth).Error
return auth, err
}
b.角色继承
// 设置角色继承关系
e := casbinService.Casbin()
e.AddRoleForUser("user1", "role1") // 用户继承角色
e.AddRoleForUser("role1", "role2") // 角色继承角色
b.API权限管理
a.分配API权限
// UpdateCasbinApi 更新API权限
func (casbinService *CasbinService) UpdateCasbinApi(oldPath string, newPath string, oldMethod string, newMethod string) error {
err := global.GVA_DB.Model(&gormadapter.CasbinRule{}).Where("v1 = ? AND v2 = ?", oldPath, oldMethod).Updates(map[string]interface{}{
"v1": newPath,
"v2": newMethod,
}).Error
e := casbinService.Casbin()
err = e.LoadPolicy()
return err
}
b.API权限验证
// 权限验证示例
func checkPermission(userId, path, method string) bool {
e := casbinService.Casbin()
// 获取用户角色
roles := e.GetRolesForUser(userId)
// 检查权限
for _, role := range roles {
if ok, _ := e.Enforce(role, path, method); ok {
return true
}
}
return false
}
c.菜单权限管理
a.菜单权限表 (sys_base_menus)
type SysBaseMenu struct {
global.GVA_MODEL
MenuLevel uint `json:"-"`
ParentId string `json:"parentId" gorm:"comment:父菜单ID"`
Path string `json:"path" gorm:"comment:路由path"`
Name string `json:"name" gorm:"comment:路由name"`
Hidden bool `json:"hidden" gorm:"comment:是否在列表隐藏"`
Component string `json:"component" gorm:"comment:对应前端文件路径"`
Sort int `json:"sort" gorm:"comment:排序标记"`
Meta `json:"meta" gorm:"embedded;comment:附加属性"`
SysAuthoritys []SysAuthority `json:"authoritys" gorm:"many2many:sys_authority_menus;"`
Children []SysBaseMenu `json:"children" gorm:"-"`
Parameters []SysBaseMenuParameter `json:"parameters"`
MenuBtn []SysBaseMenuBtn `json:"menuBtn"`
}
b.动态菜单生成
// GetMenuTree 获取动态菜单树
func (menuService *MenuService) GetMenuTree(authorityId string) (menus []system.SysMenu, err error) {
menuTree, err := menuService.getMenuTreeMap(authorityId)
menus = menuTree["0"]
for i := 0; i < len(menus); i++ {
err = menuService.getChildrenList(&menus[i], menuTree)
}
return menus, err
}
d.按钮权限管理
a.按钮权限表 (sys_base_menu_btns)
type SysBaseMenuBtn struct {
global.GVA_MODEL
Name string `json:"name" gorm:"comment:按钮关键key"`
Desc string `json:"desc" gorm:"comment:按钮备注"`
SysBaseMenuID uint `json:"sysBaseMenuID" gorm:"comment:菜单ID"`
}
b.前端按钮权限控制
<template>
<!-- 使用 v-auth 指令控制按钮显示 -->
<el-button v-auth="'user:create'" @click="createUser">
创建用户
</el-button>
<el-button v-auth="'user:delete'" @click="deleteUser">
删除用户
</el-button>
</template>
<script>
// 权限指令实现
app.directive('auth', {
mounted(el, binding) {
const { value } = binding
const userStore = useUserStore()
if (!userStore.hasPermission(value)) {
el.style.display = 'none'
}
}
})
</script>
06.权限同步机制
a.权限缓存更新
// 权限变更时同步缓存
func (casbinService *CasbinService) FreshCasbin() (err error) {
e := casbinService.Casbin()
err = e.LoadPolicy()
return err
}
// 清除用户权限缓存
func (casbinService *CasbinService) ClearUserCache(userId string) {
// 清除Redis中的用户权限缓存
global.GVA_REDIS.Del(context.Background(), "user:permissions:"+userId)
}
b.实时权限验证
// 实时权限检查
func (casbinService *CasbinService) CheckPermission(userId, resource, action string) bool {
// 1. 检查缓存
cacheKey := fmt.Sprintf("permission:%s:%s:%s", userId, resource, action)
if result, err := global.GVA_REDIS.Get(context.Background(), cacheKey).Result(); err == nil {
return result == "true"
}
// 2. 实时验证
e := casbinService.Casbin()
hasPermission, _ := e.Enforce(userId, resource, action)
// 3. 缓存结果
global.GVA_REDIS.Set(context.Background(), cacheKey, hasPermission, time.Minute*5)
return hasPermission
}
07.前端权限集成
a.路由权限控制
// router/permission.js
import { useUserStore } from '@/pinia/modules/user'
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
// 检查登录状态
if (!userStore.token) {
if (to.path !== '/login') {
return next('/login')
}
return next()
}
// 检查路由权限
if (to.meta.requiresAuth) {
const hasPermission = await userStore.checkRoutePermission(to.path)
if (!hasPermission) {
return next('/403')
}
}
next()
})
b.API权限拦截
// utils/request.js
import axios from 'axios'
import { useUserStore } from '@/pinia/modules/user'
// 请求拦截器
axios.interceptors.request.use(
config => {
const userStore = useUserStore()
// 添加 Token
if (userStore.token) {
config.headers['x-token'] = userStore.token
}
return config
},
error => Promise.reject(error)
)
// 响应拦截器
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 403) {
// 权限不足处理
ElMessage.error('权限不足')
return Promise.reject(error)
}
if (error.response?.status === 401) {
// Token 过期处理
const userStore = useUserStore()
userStore.logout()
router.push('/login')
}
return Promise.reject(error)
}
)
08.安全最佳实践
a.最小权限原则
用户只获得完成工作所需的最小权限
定期审查和清理不必要的权限
实现权限的时效性控制
b.权限分离
管理权限与业务权限分离
读权限与写权限分离
敏感操作需要额外验证
c.审计日志
// 权限操作日志
type PermissionLog struct {
UserID string `json:"user_id"`
Action string `json:"action"`
Resource string `json:"resource"`
Result bool `json:"result"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Timestamp time.Time `json:"timestamp"`
}
// 记录权限操作
func LogPermissionCheck(userID, action, resource string, result bool, c *gin.Context) {
log := PermissionLog{
UserID: userID,
Action: action,
Resource: resource,
Result: result,
IP: c.ClientIP(),
UserAgent: c.GetHeader("User-Agent"),
Timestamp: time.Now(),
}
// 记录到数据库或日志文件
global.GVA_LOG.Info("Permission Check", zap.Any("log", log))
}
09.常见问题
a.权限修改后不生效?
需要调用 e.LoadPolicy() 重新加载权限策略,或重启应用。
b.如何实现数据权限控制?
可以在 Casbin 规则中添加数据范围字段,或使用自定义的数据过滤器。
c.权限验证性能如何优化?
使用 Redis 缓存权限结果,设置合理的缓存过期时间。
d.如何实现临时权限?
可以在权限规则中添加时间字段,或使用定时任务清理过期权限。
4.5 代码生成器
01.功能概述
a.生成内容
后端代码:Model、Service、API、Router
前端代码:Vue 页面、API 接口、路由配置
数据库:自动建表、字段验证
权限配置:API 权限、菜单权限
b.支持特性
增删改查:完整的 CRUD 操作
条件查询:支持多字段条件筛选
分页查询:自动分页处理
文件上传:支持文件字段处理
关联查询:支持表关联操作
自定义模板:可自定义代码模板
02.使用方式
a.Web界面生成
a.访问系统管理 → 代码生成器,通过可视化界面配置
系统工具 → 代码生成器 → 新增
b.配置步骤
a.基础信息配置
表名称
表描述
结构体名称
包名
文件名
b.字段配置
字段名称
字段类型
数据库类型
字段描述
是否必填
查询条件
字典类型
c.生成选项
生成模块
生成路径
是否覆盖
b.命令行生成
# 进入项目目录
cd server
# 运行代码生成器
go run main.go -c=config.yaml -gva=gen
c.API接口生成
# 使用 curl 调用生成接口
curl -X POST "http://localhost:8888/autoCode/createTemp" \
-H "Content-Type: application/json" \
-d '{
"structName": "User",
"tableName": "sys_users",
"packageName": "system",
"fields": [
{
"fieldName": "Name",
"fieldDesc": "用户名",
"fieldType": "string",
"dataType": "varchar(255)",
"fieldJson": "name",
"require": true,
"errorText": "请输入用户名"
}
]
}'
03.配置详解
a.基础配置结构
type AutoCodeStruct struct {
StructName string `json:"structName"` // 结构体名称
TableName string `json:"tableName"` // 表名
PackageName string `json:"packageName"` // 包名
HumpPackageName string `json:"humpPackageName"` // 驼峰包名
Abbreviation string `json:"abbreviation"` // 缩写
Description string `json:"description"` // 描述
AutoCreateApiToSql bool `json:"autoCreateApiToSql"` // 自动创建API
AutoCreateResource bool `json:"autoCreateResource"` // 自动创建资源
AutoMoveFile bool `json:"autoMoveFile"` // 自动移动文件
Fields []Field `json:"fields"` // 字段列表
DictTypes []string `json:"dictTypes"` // 字典类型
Package string `json:"package"` // 完整包路径
PackageT string `json:"packageT"` // 模板包路径
NeedValid bool `json:"needValid"` // 需要验证
HasTimer bool `json:"hasTimer"` // 包含时间字段
NeedSort bool `json:"needSort"` // 需要排序
HasSearchTimer bool `json:"hasSearchTimer"` // 包含搜索时间
HasFile bool `json:"hasFile"` // 包含文件字段
HasDataSource bool `json:"hasDataSource"` // 包含数据源
HasRichText bool `json:"hasRichText"` // 包含富文本
HasPic bool `json:"hasPic"` // 包含图片
LogicDelete bool `json:"logicDelete"` // 逻辑删除
MenuID string `json:"menuID"` // 菜单ID
}
b.字段配置结构
type Field struct {
FieldName string `json:"fieldName"` // 字段名
FieldDesc string `json:"fieldDesc"` // 字段描述
FieldType string `json:"fieldType"` // Go字段类型
FieldJson string `json:"fieldJson"` // JSON标签
DataType string `json:"dataType"` // 数据库字段类型
ColumnName string `json:"columnName"` // 数据库列名
Comment string `json:"comment"` // 注释
Require bool `json:"require"` // 是否必填
ErrorText string `json:"errorText"` // 错误提示
Clearable bool `json:"clearable"` // 是否可清空
Sort bool `json:"sort"` // 是否排序字段
PrimaryKey bool `json:"primaryKey"` // 是否主键
DefaultValue string `json:"defaultValue"` // 默认值
DictType string `json:"dictType"` // 字典类型
Front bool `json:"front"` // 前端显示
Desc bool `json:"desc"` // 降序
SearchType string `json:"searchType"` // 搜索类型
FieldSearchType string `json:"fieldSearchType"` // 字段搜索类型
FieldSearchHide bool `json:"fieldSearchHide"` // 隐藏搜索
FieldIndexType string `json:"fieldIndexType"` // 索引类型
CheckDataSource bool `json:"checkDataSource"` // 检查数据源
DataSource struct {
Association int `json:"association"` // 关联类型
Table string `json:"table"` // 关联表
Label string `json:"label"` // 显示字段
Value string `json:"value"` // 值字段
} `json:"dataSource"` // 数据源配置
}
04.模板系统
a.模板目录结构
server/resource/template/
├── web/
│ ├── api.js.tpl # 前端API模板
│ ├── form.vue.tpl # 表单页面模板
│ ├── table.vue.tpl # 列表页面模板
│ └── ...
└── server/
├── api.go.tpl # 后端API模板
├── model.go.tpl # 模型模板
├── request.go.tpl # 请求结构模板
├── router.go.tpl # 路由模板
├── service.go.tpl # 服务模板
└── ...
b.自定义模板
a.后端模板示例
// model.go.tpl
package {{.PackageName}}
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
{{- if .HasTimer}}
"time"
{{- end}}
)
// {{.StructName}} 结构体 {{.Description}}
type {{.StructName}} struct {
global.GVA_MODEL {{- range .Fields}}
{{- if .PrimaryKey}}
{{.FieldName}} {{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"primarykey{{- if .Comment}};comment:{{.Comment}}{{- end}}"`
{{- else}}
{{.FieldName}} {{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if .ColumnName}}column:{{.ColumnName}};{{- end}}{{- if .Comment}}comment:{{.Comment}};{{- end}}{{- if .DataType}}type:{{.DataType}};{{- end}}{{- if .DefaultValue}}default:{{.DefaultValue}};{{- end}}"`
{{- end}}{{- end}}
}
// TableName {{.StructName}} 表名
func ({{.StructName}}) TableName() string {
return "{{.TableName}}"
}
b.前端模板示例
<!-- table.vue.tpl -->
<template>
<div>
<div class="gva-search-box">
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" class="demo-form-inline" :rules="searchRule" @keyup.enter="onSubmit">
{{- range .Fields}}
{{- if .FieldSearchType}}
<el-form-item label="{{.FieldDesc}}" prop="{{.FieldJson}}">
{{- if eq .FieldSearchType "LIKE" }}
<el-input v-model="searchInfo.{{.FieldJson}}" placeholder="搜索条件" />
{{- else if eq .FieldSearchType "BETWEEN" }}
<template v-if="searchInfo.{{.FieldJson}} && searchInfo.{{.FieldJson}}.length === 2">
<el-date-picker
v-model="searchInfo.{{.FieldJson}}"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</template>
{{- end}}
</el-form-item>
{{- end}}
{{- end}}
<el-form-item>
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
<el-button icon="refresh" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" icon="plus" @click="openDialog">新增</el-button>
<el-button icon="delete" style="margin-left: 10px;" :disabled="!multipleSelection.length" @click="onDelete">删除</el-button>
</div>
<el-table
ref="multipleTable"
style="width: 100%"
tooltip-effect="dark"
:data="tableData"
row-key="ID"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
{{- range .Fields}}
{{- if .Front}}
<el-table-column align="left" label="{{.FieldDesc}}" prop="{{.FieldJson}}" width="120" />
{{- end}}
{{- end}}
<el-table-column align="left" label="操作">
<template #default="scope">
<el-button type="primary" link class="table-button" @click="getDetails(scope.row)"><icon name="edit" />变更</el-button>
<el-button type="primary" link class="table-button" @click="deleteRow(scope.row)"><icon name="delete" />删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</div>
</template>
05.生成器核心代码
a.代码生成服务
type AutoCodeService struct{}
// CreateTemp 创建代码
func (autoCodeService *AutoCodeService) CreateTemp(autoCode system.AutoCodeStruct, ids ...uint) (err error) {
// 1. 创建基础目录
basePath := "autocode_template/"
if autoCode.AutoMoveFile {
basePath = ""
}
// 2. 生成后端代码
for _, value := range autoCode.Fields {
if value.FieldType == "time.Time" {
autoCode.HasTimer = true
}
if value.FieldSearchType != "" {
autoCode.NeedSort = true
}
if value.DictType != "" {
autoCode.DictTypes = append(autoCode.DictTypes, value.DictType)
}
}
// 3. 解析模板并生成文件
templates := []string{
"server/api.go.tpl",
"server/model.go.tpl",
"server/request.go.tpl",
"server/router.go.tpl",
"server/service.go.tpl",
"web/api.js.tpl",
"web/table.vue.tpl",
"web/form.vue.tpl",
}
for _, tmpl := range templates {
if err = autoCodeService.generateFile(tmpl, autoCode, basePath); err != nil {
return err
}
}
// 4. 自动注入路由和API
if autoCode.AutoCreateApiToSql {
if err = autoCodeService.AutoCreateApi(&autoCode); err != nil {
return err
}
}
return nil
}
// generateFile 生成单个文件
func (autoCodeService *AutoCodeService) generateFile(templatePath string, data system.AutoCodeStruct, basePath string) error {
// 读取模板文件
templateContent, err := ioutil.ReadFile("resource/template/" + templatePath)
if err != nil {
return err
}
// 解析模板
tmpl, err := template.New("autocode").Parse(string(templateContent))
if err != nil {
return err
}
// 生成目标文件路径
outputPath := autoCodeService.getOutputPath(templatePath, data, basePath)
// 创建目录
if err = os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
return err
}
// 生成文件
file, err := os.Create(outputPath)
if err != nil {
return err
}
defer file.Close()
// 执行模板
return tmpl.Execute(file, data)
}
b.自动注入功能
// AutoCreateApi 自动创建API
func (autoCodeService *AutoCodeService) AutoCreateApi(autoCode *system.AutoCodeStruct) error {
apis := []system.SysApi{
{
Path: "/" + autoCode.Abbreviation + "/create" + autoCode.StructName,
Description: "新增" + autoCode.Description,
ApiGroup: autoCode.Description,
Method: "POST",
},
{
Path: "/" + autoCode.Abbreviation + "/delete" + autoCode.StructName,
Description: "删除" + autoCode.Description,
ApiGroup: autoCode.Description,
Method: "DELETE",
},
{
Path: "/" + autoCode.Abbreviation + "/delete" + autoCode.StructName + "ByIds",
Description: "批量删除" + autoCode.Description,
ApiGroup: autoCode.Description,
Method: "DELETE",
},
{
Path: "/" + autoCode.Abbreviation + "/update" + autoCode.StructName,
Description: "更新" + autoCode.Description,
ApiGroup: autoCode.Description,
Method: "PUT",
},
{
Path: "/" + autoCode.Abbreviation + "/find" + autoCode.StructName,
Description: "根据ID获取" + autoCode.Description,
ApiGroup: autoCode.Description,
Method: "GET",
},
{
Path: "/" + autoCode.Abbreviation + "/get" + autoCode.StructName + "List",
Description: "获取" + autoCode.Description + "列表",
ApiGroup: autoCode.Description,
Method: "GET",
},
}
for _, api := range apis {
if err := apiService.CreateApi(api); err != nil {
return err
}
}
return nil
}
// AutoCreateMenu 自动创建菜单
func (autoCodeService *AutoCodeService) AutoCreateMenu(autoCode *system.AutoCodeStruct) error {
menu := system.SysBaseMenu{
ParentId: "0",
Path: autoCode.Abbreviation,
Name: autoCode.StructName,
Component: "view/" + autoCode.PackageName + "/" + autoCode.PackageName + ".vue",
Sort: 999,
Meta: system.Meta{
Title: autoCode.Description,
Icon: "setting",
},
}
return menuService.AddBaseMenu(menu)
}
06.高级功能
a.关联表生成
// 一对多关联配置
type Association struct {
Type string `json:"type"` // 关联类型:hasOne, hasMany, belongsTo
ForeignKey string `json:"foreignKey"` // 外键
References string `json:"references"` // 引用字段
Table string `json:"table"` // 关联表
StructName string `json:"structName"` // 关联结构体
}
// 生成关联查询代码
func generateAssociationCode(associations []Association) string {
var code strings.Builder
for _, assoc := range associations {
switch assoc.Type {
case "hasMany":
code.WriteString(fmt.Sprintf(
"db.Preload(\"%s\").Find(&result)\n",
assoc.StructName,
))
case "belongsTo":
code.WriteString(fmt.Sprintf(
"db.Preload(\"%s\").Find(&result)\n",
assoc.StructName,
))
}
}
return code.String()
}
b.字典类型支持
// 字典字段处理
func processDictField(field Field) string {
if field.DictType != "" {
return fmt.Sprintf(`
<el-select v-model="formData.%s" placeholder="请选择%s">
<el-option
v-for="item in %sOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>`,
field.FieldJson,
field.FieldDesc,
field.DictType,
)
}
return fmt.Sprintf(`<el-input v-model="formData.%s" placeholder="请输入%s" />`,
field.FieldJson, field.FieldDesc)
}
c.文件上传字段
// 文件上传字段模板
const fileUploadTemplate = `
<el-upload
class="upload-demo"
:action="path"
:headers="{ 'x-token': userStore.token }"
:on-success="handleFileSuccess"
:before-upload="beforeFileUpload"
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
</template>
</el-upload>
`
// 生成文件上传处理函数
func generateFileUploadMethods(fields []Field) string {
var methods strings.Builder
for _, field := range fields {
if field.FieldType == "file" {
methods.WriteString(fmt.Sprintf(`
// %s文件上传成功回调
const handle%sSuccess = (response) => {
formData.value.%s = response.data.file.url
}
// %s文件上传前校验
const before%sUpload = (file) => {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG) {
ElMessage.error('上传头像图片只能是 JPG/PNG 格式!')
}
if (!isLt2M) {
ElMessage.error('上传头像图片大小不能超过 2MB!')
}
return isJPG && isLt2M
}`,
strings.Title(field.FieldJson),
field.FieldJson,
field.FieldDesc,
strings.Title(field.FieldJson),
))
}
}
return methods.String()
}
07.生成统计
a.代码生成记录
type AutoCodeHistory struct {
ID uint `json:"ID" gorm:"primarykey"`
CreatedAt time.Time `json:"CreatedAt"`
UpdatedAt time.Time `json:"UpdatedAt"`
StructName string `json:"structName" gorm:"comment:结构体名称"`
TableName string `json:"tableName" gorm:"comment:表名"`
RequestMeta string `json:"requestMeta" gorm:"type:text;comment:请求meta信息"`
AutoCodePath string `json:"autoCodePath" gorm:"comment:自动生成代码路径"`
InjectionMeta string `json:"injectionMeta" gorm:"type:text;comment:注入meta信息"`
ApiIDs string `json:"apiIDs" gorm:"comment:api表注册内容"`
Flag int `json:"flag" gorm:"comment:表示对应状态 0 代表创建, 1 代表回滚 ...."`
}
// 记录生成历史
func (autoCodeService *AutoCodeService) CreateAutoCodeHistory(meta, path, injectionMeta, apiIDs string, autoCode system.AutoCodeStruct) error {
return global.GVA_DB.Create(&system.SysAutoCodeHistory{
RequestMeta: meta,
AutoCodePath: path,
InjectionMeta: injectionMeta,
StructName: autoCode.StructName,
TableName: autoCode.TableName,
ApiIDs: apiIDs,
Flag: 0,
}).Error
}
b.代码回滚
// RollBack 回滚自动生成的代码
func (autoCodeService *AutoCodeService) RollBack(id uint) error {
var history system.SysAutoCodeHistory
if err := global.GVA_DB.First(&history, id).Error; err != nil {
return err
}
// 删除生成的文件
var meta system.AutoCodeStruct
if err := json.Unmarshal([]byte(history.RequestMeta), &meta); err != nil {
return err
}
// 删除后端文件
serverFiles := []string{
fmt.Sprintf("app/%s/model/%s.go", meta.PackageName, meta.PackageName),
fmt.Sprintf("app/%s/request/%s.go", meta.PackageName, meta.PackageName),
fmt.Sprintf("app/%s/service/%s.go", meta.PackageName, meta.PackageName),
fmt.Sprintf("app/%s/api/%s.go", meta.PackageName, meta.PackageName),
fmt.Sprintf("app/%s/router/%s.go", meta.PackageName, meta.PackageName),
}
for _, file := range serverFiles {
if err := os.Remove(file); err != nil && !os.IsNotExist(err) {
return err
}
}
// 删除前端文件
webFiles := []string{
fmt.Sprintf("../web/src/api/%s.js", meta.PackageName),
fmt.Sprintf("../web/src/view/%s/%s.vue", meta.PackageName, meta.PackageName),
fmt.Sprintf("../web/src/view/%s/%sForm.vue", meta.PackageName, meta.PackageName),
}
for _, file := range webFiles {
if err := os.Remove(file); err != nil && !os.IsNotExist(err) {
return err
}
}
// 删除API记录
var apiIDs []uint
if err := json.Unmarshal([]byte(history.ApiIDs), &apiIDs); err == nil {
global.GVA_DB.Delete(&system.SysApi{}, apiIDs)
}
// 更新历史记录状态
return global.GVA_DB.Model(&history).Update("flag", 1).Error
}
08.前端集成
a.代码生成器页面
<template>
<div class="auto-code">
<el-card>
<template #header>
<div class="card-header">
<span>代码生成器</span>
</div>
</template>
<el-form ref="autoCodeForm" :model="form" :rules="rules" label-width="120px">
<el-form-item label="结构体名称" prop="structName">
<el-input v-model="form.structName" placeholder="请输入结构体名称" />
</el-form-item>
<el-form-item label="表名" prop="tableName">
<el-input v-model="form.tableName" placeholder="请输入表名" />
</el-form-item>
<el-form-item label="包名" prop="packageName">
<el-input v-model="form.packageName" placeholder="请输入包名" />
</el-form-item>
<el-form-item label="结构体描述" prop="description">
<el-input v-model="form.description" placeholder="请输入结构体描述" />
</el-form-item>
<!-- 字段配置 -->
<el-form-item label="字段配置">
<el-table :data="form.fields" border>
<el-table-column label="字段名" prop="fieldName" />
<el-table-column label="字段描述" prop="fieldDesc" />
<el-table-column label="字段类型" prop="fieldType" />
<el-table-column label="数据库类型" prop="dataType" />
<el-table-column label="操作">
<template #default="scope">
<el-button @click="editField(scope.$index)">编辑</el-button>
<el-button @click="deleteField(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-button @click="addField" style="margin-top: 10px;">添加字段</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="generateCode">生成代码</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { createTemp } from '@/api/autoCode'
import { ElMessage } from 'element-plus'
const form = reactive({
structName: '',
tableName: '',
packageName: '',
description: '',
fields: []
})
const rules = {
structName: [{ required: true, message: '请输入结构体名称', trigger: 'blur' }],
tableName: [{ required: true, message: '请输入表名', trigger: 'blur' }],
packageName: [{ required: true, message: '请输入包名', trigger: 'blur' }]
}
const generateCode = async () => {
try {
const res = await createTemp(form)
if (res.code === 0) {
ElMessage.success('代码生成成功')
}
} catch (error) {
ElMessage.error('代码生成失败')
}
}
</script>
09.常见问题
a.生成的代码编译失败?
检查字段类型配置是否正确,确保 Go 类型和数据库类型匹配。
b.前端页面显示异常?
检查字段的前端显示配置,确保必要的字段已设置为前端显示。
c.如何自定义生成模板?
修改 server/resource/template/ 目录下的模板文件,重启服务即可生效。
d.生成后如何添加自定义逻辑?
在生成的代码基础上添加自定义方法,避免直接修改生成的核心 CRUD 方法。
4.6 对象存储
01.说明
当前支持本地、阿里云、腾讯云、七牛云、AWS S3、华为云、Cloudflare R2 七种对象存储方式,可根据实际情况进行配置。
02.配置
a.分类1
local:
path: uploads/file
store-path: uploads/file
b.分类2
aliyun-oss:
endpoint: yourEndpoint
access-key-id: yourAccessKeyId
access-key-secret: yourAccessKeySecret
bucket-name: yourBucketName
bucket-url: yourBucketUrl
base-path: yourBasePath
c.分类3
aws-s3:
bucket: xxxxx-10005608
region: ap-shanghai
endpoint: ""
secret-id: your-secret-id
secret-key: your-secret-key
base-url: https://gin.vue.admin
path-prefix: github.com/flipped-aurora/gin-vue-admin/server
s3-force-path-style: false
disable-ssl: false
d.分类4
qiniu:
zone: ZoneHuaDong
bucket: ""
img-path: ""
access-key: ""
secret-key: ""
use-https: false
use-cdn-domains: false
e.分类5
tencent-cos:
bucket: xxxxx-10005608
region: ap-shanghai
secret-id: your-secret-id
secret-key: your-secret-key
base-url: https://gin.vue.admin
path-prefix: github.com/flipped-aurora/gin-vue-admin/server
f.分类6
hua-wei-obs:
path: you-path
bucket: you-bucket
endpoint: you-endpoint
access-key: you-access-key
secret-key: you-secret-key
g.分类7
cloudflare-r2:
bucket: xxxx0bucket
base-url: https://gin.vue.admin.com
path: uploads
account-id: xxx_account_id
access-key-id: xxx_key_id
secret-access-key: xxx_secret_key
4.7 多数据库支持
01.文件配置
a.说明
在配置文件中新增db-list选项 db-list是文件中mysql与pymysql的一个超集,为了兼容低版本,目前没有在页面初始化的时候直接生成到db-list中
b.代码
db-list: [
{
disabled: false, # 是否禁用,填ture将不被初始化
type: "", # 数据库的类型,目前支持mysql、pgsql
alias-name: "", # 数据库的名称,注意: alias-name 需要在db-list中唯一
path: '',
port: '',
config: '',
db-name: '',
username: '',
password: '',
max-idle-conns: 10,
max-open-conns: 100,
log-mode: "",
log-zap: false,
}
]
02.使用
a.说明
在config.yaml中正确配置db-list参数后,在main文件中添加初始化方法
initialize.DBList() # 初始化多数据库列表
b.说明
使用时根据配置的alias-name从 global.GetGlobalDBByDBName(alias-name)
或者global.MustGetGlobalDBByDBName(alias-name)方法中获取db对象,
两个方法的区别是MustGetGlobalDBByDBName会在alias-name对应db对象不存在时panic
4.8 严格角色模式
01.介绍
开启严格角色模式后,用户只能访问其拥有的角色权限,无法访问其他角色的权限。
02.配置
system:
db-type: mysql
oss-type: local
router-prefix: ""
addr: 8888
iplimit-count: 15000
iplimit-time: 3600
use-multipoint: false
use-redis: false
use-mongo: false
use-strict-auth: true # 这里修改为true
03.使用
如果为顶级角色 则可以看到自己的角色且可以分配自己角色的相关api权限和菜单权限
如果为子角色 则无法看到自己角色,能看到自己角色的下级所有角色,
且可以对下级以及下级的所有角色做权限分配,分配范围为自己所有用的角色的所有权限
4.9 viper
01.优先级说明
a.说明
path 可变参数是为了给单元测试留下一个口子,方便使用相对路径或者绝对路径指定config文件所在的位置
使用 ./server -c xxx/config.yaml 使用命令行进行传递的值赋值给config变量
ConfigEnv 是定义在server/core/internal/constant.go 的一个常量,可自行修改为自己想要的环境变量
最后会按照 Gin 框架自带的环境变量 GIN_MODE 进行匹配 server/core/internal/constant.go 文件中的定义。
b.注意
GIN_MODE 只能有三个值,debug、release、test, 其他值会panic的
02.GIN_MODE 使用场景说明
a.说明
有三个分支,开发分支 develop,测试分支 test ,生产分支 release
但是三个分支的链接的数据库,oss都是不同的,所以就会有三个配置文件,这个不可能用文档保存的
所以一般使用git的.gitattributes文件,每个分支都有属于自己的分支的配置文件以及Dockerfile
在Dockerfile文件里指定是以下的任意一行代码即可,这样就可以控制每种环境对应的配置文件
b.代码
ENV GIN_MODE=debug
ENV GIN_MODE=release
ENV GIN_MODE=test
4.10 zap
01.基于zap的扩展路由日志中间件
a.说明
在 server/middleware 目录新建一个 logger.go 文件
将以下代码复制粘贴进 logger.go 文件
b.代码
package middleware
import (
"gin-vue-admin/global"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"time"
)
// ZapLogger 接收gin框架的路由日志
func ZapLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
cost := time.Since(start)
global.GVA_LOG.Info(path,
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
zap.Duration("cost", cost),
)
}
}
c.使用ZapLogger()中间件
var Router = gin.Default()
// 将上面的代码替换为
var Router = gin.New()
Router.Use(middleware.ZapLogger(), gin.Recovery())
d.注意事项
在 server/initialize/router.go 文件中
一定要 ZapLogger 中间件 注册为全局中间件 ,不然不会生效的
gin.Recovery() 与 Logger() 为Gin框架的 gin.Default() 默认使用的全局中间件
-----------------------------------------------------------------------------------------------------
// 此代码为Gin框架的源码
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
02.基于zap的Recovery捕获panic异常中间件
a.说明
在 server/middleware 目录新建一个 logger.go 文件,将以下代码复制粘贴进 logger.go 文件
b.代码
package middleware
import (
"gin-vue-admin/global"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
)
// ZapRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
func ZapRecovery(stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
global.GVA_LOG.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// If the connection is dead, we can't write a status to it.
_ = c.Error(err.(error)) // nolint: errcheck
c.Abort()
return
}
if stack {
global.GVA_LOG.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
} else {
global.GVA_LOG.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
}
}
c.v2.3.0版本以上无需添加此文件代码
a.使用ZapRecovery()中间件
var Router = gin.Default()
// 将上面的代码替换为
var Router = gin.New()
Router.Use(middleware.ZapLogger(), middleware.ZapRecovery())
// V2.3.0版本请使用以下代码
var Router = gin.Default()
// 将上面的代码替换为
var Router = gin.New()
Router.Use(middleware.ZapLogger(), middleware.GinRecovery())
b.Zap日志库使用指南&&配置指南
// Zap日志库的配置选择在 config.yaml (./server/config.yaml)下的zap
# zap logger configuration
zap:
level: 'debug'
format: 'console'
prefix: '[GIN-VUE-ADMIN]'
director: 'log'
link_name: 'latest_log'
show_line: true
encode_level: 'LowercaseColorLevelEncoder'
stacktrace_key: 'stacktrace'
log_in_console: true
-------------------------------------------------------------------------------------------------
配置名 配置的类型 说明
level string level的模式的详细说明,请看zap官方文档
info: info模式,无错误的堆栈信息,只输出信息
debug: debug模式,有错误的堆栈详细信息
warn: warn模式
error: error模式,有错误的堆栈详细信息
dpanic: dpanic模式
panic: panic模式
fatal: fatal模式
format string console: 控制台形式输出日志
json: json格式输出日志
prefix string 日志的前缀
director string 存放日志的文件夹,修改即可,不需要手动创建
link_name string 在server目录下会生成一个link_name的软连接文件,链接的是director配置项的最新日志文件
show_line bool 显示行号, 默认为true,不建议修改
encode_level string LowercaseLevelEncoder:小写
LowercaseColorLevelEncoder:小写带颜色
CapitalLevelEncoder: 大写
CapitalColorLevelEncoder: 大写带颜色
stacktrace_key string 堆栈的名称,即在json格式输出日志时的josn的key
log_in_console bool 是否输出到控制台,默认为true
-------------------------------------------------------------------------------------------------
开发环境 || 调试环境配置建议
level:debug
format:console
encode_level:LowercaseColorLevelEncoder或者encode_leve:CapitalColorLevelEncoder
-------------------------------------------------------------------------------------------------
部署环境配置建议
level:error
format:json
encode_level: LowercaseLevelEncoder 或者 encode_level:CapitalLevelEncoder
log_in_console: false
-------------------------------------------------------------------------------------------------
建议只是建议,按照自己的需求进行即可,给出建议仅供参考
4.11 gorm
01.mysql
a.server/config/gorm_mysql.go
a.gorm-mysql
package config
type Mysql struct {
Path string `mapstructure:"path" json:"path" yaml:"path"` // 服务器地址
Port string `mapstructure:"port" json:"port" yaml:"port"` // 端口
Config string `mapstructure:"config" json:"config" yaml:"config"` // 高级配置
Dbname string `mapstructure:"db-name" json:"dbname" yaml:"db-name"` // 数据库名
Username string `mapstructure:"username" json:"username" yaml:"username"` // 数据库用户名
Password string `mapstructure:"password" json:"password" yaml:"password"` // 数据库密码
MaxIdleConns int `mapstructure:"max-idle-conns" json:"maxIdleConns" yaml:"max-idle-conns"` // 空闲中的最大连接数
MaxOpenConns int `mapstructure:"max-open-conns" json:"maxOpenConns" yaml:"max-open-conns"` // 打开到数据库的最大连接数
LogMode string `mapstructure:"log-mode" json:"logMode" yaml:"log-mode"` // 是否开启Gorm全局日志
LogZap bool `mapstructure:"log-zap" json:"logZap" yaml:"log-zap"` // 是否通过zap写入日志文件
}
02.pgsql
a.server/config/gorm_pgsql.go
a.gorm-pgsql
package config
type Pgsql struct {
Path string `mapstructure:"path" json:"path" yaml:"path"` // 服务器地址:端口
Port string `mapstructure:"port" json:"port" yaml:"port"` //:端口
Config string `mapstructure:"config" json:"config" yaml:"config"` // 高级配置
Dbname string `mapstructure:"db-name" json:"dbname" yaml:"db-name"` // 数据库名
Username string `mapstructure:"username" json:"username" yaml:"username"` // 数据库用户名
Password string `mapstructure:"password" json:"password" yaml:"password"` // 数据库密码
MaxIdleConns int `mapstructure:"max-idle-conns" json:"maxIdleConns" yaml:"max-idle-conns"` // 空闲中的最大连接数
MaxOpenConns int `mapstructure:"max-open-conns" json:"maxOpenConns" yaml:"max-open-conns"` // 打开到数据库的最大连接数
LogMode string `mapstructure:"log-mode" json:"logMode" yaml:"log-mode"` // 是否开启Gorm全局日志
LogZap bool `mapstructure:"log-zap" json:"logZap" yaml:"log-zap"` // 是否通过zap写入日志文件
}
b.server/config/config.go
a.在system选项下 选择db-type为mysql或者pgsql
system:
env: 'public' # Change to "develop" to skip authentication for development mode
addr: 8888
db-type: 'mysql'
oss-type: 'local' # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置
use-multipoint: false
# IP限制次数 一个小时15000次
iplimit-count: 15000
# IP限制一个小时
iplimit-time: 3600
b.config.yaml 配置字段详解
mysql:
path: '' # 链接地址
port: '' # 链接端口
config: '' # 其他配置 例如时区
db-name: '' # 数据库名称
username: '' # 数据库用户名
password: '' # 数据库密码
max-idle-conns: 10 # 连接池相关
max-open-conns: 100 # 连接池相关
log-mode: "" # 是控制台打印日志级别 "silent"、"error"、"warn"、"info" 不填默认info 填入silent可以关闭控制台日志
log-zap: false # 日志是否用zap保存到本地
4.12 定时任务
01.接口地址 : gin-vue-admin/server/utils/timer/Timer
type Timer interface {
// 寻找所有Cron
FindCronList() map[string]*taskManager
// 添加Task 方法形式以秒的形式加入
AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) // 添加Task Func以秒的形式加入
// 添加Task 接口形式以秒的形式加入
AddTaskByJobWithSeconds(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error)
// 通过函数的方法添加任务
AddTaskByFunc(cronName string, spec string, task func(), taskName string, option ...cron.Option) (cron.EntryID, error)
// 通过接口的方法添加任务 要实现一个带有 Run方法的接口触发
AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error)
// 获取对应taskName的cron 可能会为空
FindCron(cronName string) (*taskManager, bool)
// 指定cron开始执行
StartCron(cronName string)
// 指定cron停止执行
StopCron(cronName string)
// 查找指定cron下的指定task
FindTask(cronName string, taskName string) (*task, bool)
// 根据id删除指定cron下的指定task
RemoveTask(cronName string, id int)
// 根据taskName删除指定cron下的指定task
RemoveTaskByName(cronName string, taskName string)
// 清理掉指定cronName
Clear(cronName string)
// 停止所有的cron
Close()
}
02.使用Demo
type Job struct{}
func (j *Job) Run() {
fmt.Println("testFunc") // 每天打印一遍
}
func Demo() {
// 在gva中 global.GVA_Timer 已经是初始化Timer对象供使用
// 如果你想自己调用 utils.NewTimerTask() 即可拿到 Timer 接口
// demo 演示
t := utils.NewTimerTask()
// spec 定时任务详细配置参考 https://pkg.go.dev/github.com/robfig/cron?utm_source=godoc
// 同一个 taskName 中也可以设置多个任务
id , err := t.AddTaskByFunc("testFunc", "@daily", func() {
fmt.Println("testFunc") // 每天打印一遍
})
if err != nil {
// ...
}
// job 实例
id1 , err1 := t.AddTaskByJob("testFunc", "@daily", &Job{})
if err1 != nil {
// ...
}
// 使用 AddTaskByFunc AddTaskByJob 添加的任务默认是激活状态
// 想要停止调用 StopTask 即可
t.StopTask("testFunc")
// 也可以调用 t.FindCron() 获取cron原生对象,调用它的更多方法
// t.Remove() 删除任务
t.Remove("testFunc",id)
// t.Close() 释放资源
t.Close()
}
03.项目内示例,文件路径gin-vue-admin/server/initialize/timer.go
package initialize
import (
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/task"
"github.com/robfig/cron/v3"
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
func Timer() {
go func() {
var option []cron.Option
option = append(option, cron.WithSeconds())
// 清理DB定时任务
_, err := global.GVA_Timer.AddTaskByFunc("ClearDB", "@daily", func() {
err := task.ClearTable(global.GVA_DB) // 定时任务方法定在task文件包中
if err != nil {
fmt.Println("timer error:", err)
}
}, option...)
if err != nil {
fmt.Println("add timer error:", err)
}
// 其他定时任务定在这里 参考上方使用方法
//_, err := global.GVA_Timer.AddTaskByFunc("定时任务标识", "corn表达式", func() {
// 具体执行内容...
// ......
//}, option...)
//if err != nil {
// fmt.Println("add timer error:", err)
//}
}()
}
4.13 AI助手集成
01.项目
革命性的AI开发体验! 通过MCP(Model Context Protocol)让AI编辑工具深度理解GVA项目结构,实现智能化的代码生成和项目管理。
版本要求:使用MCP功能需要GVA版本 ≥ 2.8.4,请确保您的项目版本满足要求。
尽量使用 claude > gemini > gpt = kimi 模型 已达到更好效果
02.核心特性
智能代码生成:AI自动创建完整的CRUD模板
智能文件搜索:自动定位相关文件并提供精准修改建议
自动化流程:一键生成API接口和菜单配置
上下文理解:AI深度理解项目架构,提供更准确的代码联动
03.AI编辑工具配置
a.支持的AI编辑工具
Trae (尽量使用 trae.ai 国外版)
Cursor
Claude Code
Windsurf
Codebubby
其他支持MCP协议的AI编辑器
b.第一步:启动GVA项目
确保你的GVA项目正在运行,MCP服务会自动在 http://127.0.0.1:8888/sse 启动
c.第二步:配置AI编辑器
在你的AI编辑工具的配置文件中添加以下MCP配置:
{
"mcpServers": {
"GVA Helper": {
"url": "http://127.0.0.1:8888/sse"
}
}
}
d.第三步:重启编辑器
保存配置后重启你的AI编辑工具,等待MCP连接建立,MCP状态显示绿色即表示连接成功
e.AI助手新能力
配置完成后,AI助手将获得以下超能力:
深度理解项目:自动识别GVA项目结构和代码模式
智能代码生成:根据需求自动生成完整的功能模块
精准文件定位:快速找到相关文件并提供修改建议
全栈开发:同时处理前端、后端、数据库的代码生成
UI自动化:自动配置路由、菜单和权限系统
f.使用示例
只需要告诉AI:"我想创建一个用户管理模块",AI就会:
自动生成用户表结构
创建完整的CRUD API
生成前端管理页面
配置菜单和路由
设置权限控制
04.配置文件说明
mcp:
name: GVA_MCP # MCP服务名称
version: v1.0.0 # 版本号
sse_path: /sse # SSE路径
message_path: /message # 消息路径
url_prefix: '' # URL前缀
## v2.8.6后可用
addr: 8889 # 监听地址 在separate为true时有效 (暂时未实现独立运行功能,敬请期待)
separate: false # 是否隔离 开启以后mcp将不会跟随gva本体启动 建议在生产环境开启
4.14 数据库设计
01.数据库架构概览
a.数据库选择
Gin-Vue-Admin 支持多种数据库:
MySQL 8.0+ (推荐)
PostgreSQL 12+
SQLite 3 (开发环境)
SQL Server 2019+
b.核心模块
数据库架构
├── 系统管理模块
│ ├── 用户管理 (sys_users)
│ ├── 角色管理 (sys_authorities)
│ ├── 菜单管理 (sys_base_menus)
│ ├── API管理 (sys_apis)
│ └── 权限规则 (casbin_rule)
├── 基础功能模块
│ ├── 字典管理 (sys_dictionaries)
│ ├── 文件上传 (exa_file_upload_and_downloads)
│ ├── 操作历史 (sys_operation_records)
│ └── JWT黑名单 (jwt_blacklists)
├── 代码生成模块
│ ├── 自动代码 (sys_auto_codes)
│ └── 代码历史 (sys_auto_code_histories)
└── 示例模块
├── 客户管理 (exa_customers)
└── 文件分片 (exa_file_chunks)
02.核心表结构
a.用户管理表
a.sys_users (用户表)
CREATE TABLE `sys_users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`uuid` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户UUID',
`username` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户登录名',
`password` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户登录密码',
`nick_name` varchar(191) COLLATE utf8mb4_general_ci DEFAULT '系统用户' COMMENT '用户昵称',
`side_mode` varchar(191) COLLATE utf8mb4_general_ci DEFAULT 'dark' COMMENT '主题模式',
`header_img` varchar(191) COLLATE utf8mb4_general_ci DEFAULT 'https://qmplusimg.henrongyi.top/gva_header.jpg' COMMENT '用户头像',
`base_color` varchar(191) COLLATE utf8mb4_general_ci DEFAULT '#fff' COMMENT '基础颜色',
`active_color` varchar(191) COLLATE utf8mb4_general_ci DEFAULT '#1890ff' COMMENT '活跃颜色',
`authority_id` bigint unsigned DEFAULT '888' COMMENT '用户角色ID',
`phone` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户手机号',
`email` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户邮箱',
`enable` tinyint(1) DEFAULT '1' COMMENT '用户是否被冻结 1正常 2冻结',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_sys_users_username` (`username`),
UNIQUE KEY `idx_sys_users_uuid` (`uuid`),
KEY `idx_sys_users_deleted_at` (`deleted_at`),
KEY `idx_sys_users_authority_id` (`authority_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';
b.sys_authorities (角色表)
CREATE TABLE `sys_authorities` (
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`authority_id` bigint unsigned NOT NULL COMMENT '角色ID',
`authority_name` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '角色名',
`parent_id` bigint unsigned DEFAULT NULL COMMENT '父角色ID',
`default_router` varchar(191) COLLATE utf8mb4_general_ci DEFAULT 'dashboard' COMMENT '默认菜单',
PRIMARY KEY (`authority_id`),
UNIQUE KEY `idx_sys_authorities_authority_id` (`authority_id`),
KEY `idx_sys_authorities_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='角色表';
c.sys_user_authority (用户角色关联表)
CREATE TABLE `sys_user_authority` (
`sys_user_id` bigint unsigned NOT NULL COMMENT '用户ID',
`sys_authority_authority_id` bigint unsigned NOT NULL COMMENT '角色ID',
PRIMARY KEY (`sys_user_id`,`sys_authority_authority_id`),
KEY `fk_sys_user_authority_sys_authority` (`sys_authority_authority_id`),
CONSTRAINT `fk_sys_user_authority_sys_authority` FOREIGN KEY (`sys_authority_authority_id`) REFERENCES `sys_authorities` (`authority_id`),
CONSTRAINT `fk_sys_user_authority_sys_user` FOREIGN KEY (`sys_user_id`) REFERENCES `sys_users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户角色关联表';
b.权限管理表
a.casbin_rule (权限规则表)
CREATE TABLE `casbin_rule` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`ptype` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '策略类型',
`v0` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '角色ID',
`v1` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '资源路径',
`v2` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '请求方法',
`v3` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`v4` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`v5` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_casbin_rule` (`ptype`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Casbin权限规则表';
sys_apis (API管理表)
b.sys_apis (API管理表)
CREATE TABLE `sys_apis` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'API ID',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`path` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'API路径',
`description` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'API描述',
`api_group` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'API分组',
`method` varchar(191) COLLATE utf8mb4_general_ci DEFAULT 'POST' COMMENT '请求方法',
PRIMARY KEY (`id`),
KEY `idx_sys_apis_deleted_at` (`deleted_at`),
KEY `idx_sys_apis_path_method` (`path`,`method`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='API管理表';
sys_authority_menus (角色菜单关联表)
c.sys_authority_menus (角色菜单关联表)
CREATE TABLE `sys_authority_menus` (
`sys_authority_authority_id` bigint unsigned NOT NULL COMMENT '角色ID',
`sys_base_menu_id` bigint unsigned NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`sys_authority_authority_id`,`sys_base_menu_id`),
KEY `fk_sys_authority_menus_sys_base_menu` (`sys_base_menu_id`),
CONSTRAINT `fk_sys_authority_menus_sys_authority` FOREIGN KEY (`sys_authority_authority_id`) REFERENCES `sys_authorities` (`authority_id`),
CONSTRAINT `fk_sys_authority_menus_sys_base_menu` FOREIGN KEY (`sys_base_menu_id`) REFERENCES `sys_base_menus` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='角色菜单关联表';
c.菜单管理表
a.sys_base_menus (基础菜单表)
CREATE TABLE `sys_base_menus` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`menu_level` bigint unsigned DEFAULT NULL COMMENT '菜单层级',
`parent_id` bigint unsigned DEFAULT NULL COMMENT '父菜单ID',
`path` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '路由路径',
`name` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '路由名称',
`hidden` tinyint(1) DEFAULT NULL COMMENT '是否隐藏',
`component` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '组件路径',
`sort` bigint DEFAULT NULL COMMENT '排序',
`active_name` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '高亮菜单',
`keep_alive` tinyint(1) DEFAULT NULL COMMENT '是否缓存',
`default_menu` tinyint(1) DEFAULT NULL COMMENT '是否默认菜单',
`title` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '菜单标题',
`icon` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '菜单图标',
`close_tab` tinyint(1) DEFAULT NULL COMMENT '是否关闭标签',
PRIMARY KEY (`id`),
KEY `idx_sys_base_menus_deleted_at` (`deleted_at`),
KEY `idx_sys_base_menus_parent_id` (`parent_id`),
KEY `idx_sys_base_menus_sort` (`sort`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='基础菜单表';
b.sys_base_menu_btns (菜单按钮表)
CREATE TABLE `sys_base_menu_btns` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '按钮ID',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`name` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '按钮名称',
`desc` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '按钮描述',
`sys_base_menu_id` bigint unsigned DEFAULT NULL COMMENT '菜单ID',
PRIMARY KEY (`id`),
KEY `idx_sys_base_menu_btns_deleted_at` (`deleted_at`),
KEY `idx_sys_base_menu_btns_sys_base_menu_id` (`sys_base_menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='菜单按钮表';
d.系统功能表
a.sys_dictionaries (字典表)
CREATE TABLE `sys_dictionaries` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '字典ID',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`name` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '字典名称',
`type` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '字典类型',
`status` tinyint(1) DEFAULT NULL COMMENT '状态',
`desc` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_sys_dictionaries_type` (`type`),
KEY `idx_sys_dictionaries_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='字典表';
b.sys_dictionary_details (字典详情表)
CREATE TABLE `sys_dictionary_details` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '字典详情ID',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`label` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '展示值',
`value` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '字典值',
`extend` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '扩展值',
`status` tinyint(1) DEFAULT NULL COMMENT '启用状态',
`sort` bigint DEFAULT NULL COMMENT '排序标记',
`sys_dictionary_id` bigint unsigned DEFAULT NULL COMMENT '关联字典ID',
PRIMARY KEY (`id`),
KEY `idx_sys_dictionary_details_deleted_at` (`deleted_at`),
KEY `idx_sys_dictionary_details_sys_dictionary_id` (`sys_dictionary_id`),
KEY `idx_sys_dictionary_details_sort` (`sort`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='字典详情表';
c.sys_operation_records (操作记录表)
CREATE TABLE `sys_operation_records` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '记录ID',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`ip` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '请求IP',
`method` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '请求方法',
`path` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '请求路径',
`status` bigint DEFAULT NULL COMMENT '请求状态',
`latency` bigint DEFAULT NULL COMMENT '延迟时间',
`agent` text COLLATE utf8mb4_general_ci COMMENT '代理信息',
`error_message` text COLLATE utf8mb4_general_ci COMMENT '错误信息',
`body` text COLLATE utf8mb4_general_ci COMMENT '请求Body',
`resp` text COLLATE utf8mb4_general_ci COMMENT '响应Body',
`user_id` bigint unsigned DEFAULT NULL COMMENT '用户ID',
PRIMARY KEY (`id`),
KEY `idx_sys_operation_records_deleted_at` (`deleted_at`),
KEY `idx_sys_operation_records_user_id` (`user_id`),
KEY `idx_sys_operation_records_created_at` (`created_at`),
KEY `idx_sys_operation_records_method_path` (`method`,`path`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='操作记录表';
e.文件管理表
a.exa_file_upload_and_downloads (文件上传下载表)
CREATE TABLE `exa_file_upload_and_downloads` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '文件ID',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`name` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文件名',
`url` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文件地址',
`tag` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文件标签',
`key` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文件唯一标识',
PRIMARY KEY (`id`),
KEY `idx_exa_file_upload_and_downloads_deleted_at` (`deleted_at`),
KEY `idx_exa_file_upload_and_downloads_tag` (`tag`),
KEY `idx_exa_file_upload_and_downloads_key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文件上传下载表';
b.exa_file_chunks (文件分片表)
CREATE TABLE `exa_file_chunks` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '分片ID',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`exa_file_id` bigint unsigned DEFAULT NULL COMMENT '文件ID',
`file_chunk_number` bigint DEFAULT NULL COMMENT '分片编号',
`file_chunk_path` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '分片路径',
PRIMARY KEY (`id`),
KEY `idx_exa_file_chunks_deleted_at` (`deleted_at`),
KEY `idx_exa_file_chunks_exa_file_id` (`exa_file_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文件分片表';
f.代码生成表
a.sys_auto_codes (自动代码表)
CREATE TABLE `sys_auto_codes` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '代码ID',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`package_name` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '包名',
`label` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '展示名',
`desc` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`),
KEY `idx_sys_auto_codes_deleted_at` (`deleted_at`),
KEY `idx_sys_auto_codes_package_name` (`package_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='自动代码表';
b.sys_auto_code_histories (代码生成历史表)
CREATE TABLE `sys_auto_code_histories` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '历史ID',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`package` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '包名',
`business_db` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '业务数据库',
`table_name` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '表名',
`menu_id` bigint unsigned DEFAULT NULL COMMENT '菜单ID',
`request_meta` text COLLATE utf8mb4_general_ci COMMENT '请求元数据',
`auto_code_path` text COLLATE utf8mb4_general_ci COMMENT '自动生成代码路径',
`injection_meta` text COLLATE utf8mb4_general_ci COMMENT '注入元数据',
`struct_name` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '结构体名称',
`struct_cn_name` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '结构体中文名称',
`api_ids` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'API IDs',
`flag` bigint DEFAULT NULL COMMENT '标记',
PRIMARY KEY (`id`),
KEY `idx_sys_auto_code_histories_deleted_at` (`deleted_at`),
KEY `idx_sys_auto_code_histories_package` (`package`),
KEY `idx_sys_auto_code_histories_table_name` (`table_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='代码生成历史表';
02.表关系设计
a.用户权限关系图
用户 (sys_users)
↓ (1:N)
用户角色关联 (sys_user_authority)
↓ (N:1)
角色 (sys_authorities)
↓ (1:N)
角色菜单关联 (sys_authority_menus)
↓ (N:1)
菜单 (sys_base_menus)
↓ (1:N)
菜单按钮 (sys_base_menu_btns)
角色 (sys_authorities)
↓ (通过Casbin)
权限规则 (casbin_rule)
↓ (关联)
API (sys_apis)
b.核心关系说明
a.用户-角色关系
// 用户结构体
type SysUser struct {
global.GVA_MODEL
UUID uuid.UUID `json:"uuid" gorm:"index;comment:用户UUID"`
Username string `json:"userName" gorm:"index;comment:用户登录名"`
Password string `json:"-" gorm:"comment:用户登录密码"`
NickName string `json:"nickName" gorm:"default:系统用户;comment:用户昵称"`
SideMode string `json:"sideMode" gorm:"default:dark;comment:用户侧边主题"`
HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"`
BaseColor string `json:"baseColor" gorm:"default:#fff;comment:基础颜色"`
ActiveColor string `json:"activeColor" gorm:"default:#1890ff;comment:活跃颜色"`
AuthorityId uint `json:"authorityId" gorm:"default:888;comment:用户角色ID"`
Authority SysAuthority `json:"authority" gorm:"foreignKey:AuthorityId;references:AuthorityId;comment:用户角色"`
Authorities []SysAuthority `json:"authorities" gorm:"many2many:sys_user_authority;"`
Phone string `json:"phone" gorm:"comment:用户手机号"`
Email string `json:"email" gorm:"comment:用户邮箱"`
Enable int `json:"enable" gorm:"default:1;comment:用户是否被冻结 1正常 2冻结"`
}
b.角色-菜单关系
// 角色结构体
type SysAuthority struct {
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *gorm.DeletedAt `json:"-" gorm:"index"`
AuthorityId uint `json:"authorityId" gorm:"not null;unique;primary_key;comment:角色ID"`
AuthorityName string `json:"authorityName" gorm:"comment:角色名"`
ParentId *uint `json:"parentId" gorm:"comment:父角色ID"`
DataAuthorityId []SysAuthority `json:"dataAuthorityId" gorm:"many2many:sys_data_authority_id"`
Children []SysAuthority `json:"children" gorm:"-"`
SysBaseMenus []SysBaseMenu `json:"menus" gorm:"many2many:sys_authority_menus;"`
Users []SysUser `json:"-" gorm:"many2many:sys_user_authority;"`
DefaultRouter string `json:"defaultRouter" gorm:"comment:默认菜单"`
}
c.菜单层级关系
// 菜单结构体
type SysBaseMenu struct {
global.GVA_MODEL
MenuLevel uint `json:"-"`
ParentId *uint `json:"parentId" gorm:"comment:父菜单ID"`
Path string `json:"path" gorm:"comment:路由path"`
Name string `json:"name" gorm:"comment:路由name"`
Hidden *bool `json:"hidden" gorm:"comment:是否在列表隐藏"`
Component string `json:"component" gorm:"comment:对应前端文件路径"`
Sort int `json:"sort" gorm:"comment:排序标记"`
ActiveName string `json:"activeName" gorm:"comment:高亮菜单"`
KeepAlive *bool `json:"keepAlive" gorm:"comment:是否缓存"`
DefaultMenu *bool `json:"defaultMenu" gorm:"comment:是否是基础路由(开发中)"`
Title string `json:"title" gorm:"comment:菜单名"`
Icon string `json:"icon" gorm:"comment:菜单图标"`
CloseTab *bool `json:"closeTab" gorm:"comment:自动关闭tab"`
Authorities []SysAuthority `json:"authorities" gorm:"many2many:sys_authority_menus;"`
Children []SysBaseMenu `json:"children" gorm:"-"`
MenuBtn []SysBaseMenuBtn `json:"menuBtn"`
Parameters []SysBaseMenuParameter `json:"parameters"`
}
03.索引优化策略
a.主要索引设计
a.用户表索引
-- 主键索引
PRIMARY KEY (`id`)
-- 唯一索引
UNIQUE KEY `idx_sys_users_username` (`username`)
UNIQUE KEY `idx_sys_users_uuid` (`uuid`)
-- 普通索引
KEY `idx_sys_users_deleted_at` (`deleted_at`)
KEY `idx_sys_users_authority_id` (`authority_id`)
KEY `idx_sys_users_email` (`email`)
KEY `idx_sys_users_phone` (`phone`)
-- 复合索引
KEY `idx_sys_users_enable_deleted` (`enable`, `deleted_at`)
b.操作记录表索引
-- 时间范围查询索引
KEY `idx_sys_operation_records_created_at` (`created_at`)
-- 用户操作查询索引
KEY `idx_sys_operation_records_user_id` (`user_id`)
-- API路径查询索引
KEY `idx_sys_operation_records_method_path` (`method`, `path`)
-- 状态查询索引
KEY `idx_sys_operation_records_status` (`status`)
-- 复合查询索引
KEY `idx_operation_user_time` (`user_id`, `created_at`, `method`)
c.权限规则表索引
-- Casbin查询优化索引
UNIQUE KEY `idx_casbin_rule` (`ptype`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`)
-- 角色权限查询索引
KEY `idx_casbin_rule_v0` (`v0`)
-- 资源权限查询索引
KEY `idx_casbin_rule_v1_v2` (`v1`, `v2`)
b.查询优化示例
a.用户列表查询优化
-- 优化前:全表扫描
SELECT * FROM sys_users
WHERE deleted_at IS NULL
AND enable = 1
ORDER BY created_at DESC
LIMIT 10 OFFSET 0;
-- 优化后:使用复合索引
SELECT id, username, nick_name, email, phone, created_at
FROM sys_users
WHERE enable = 1 AND deleted_at IS NULL
ORDER BY created_at DESC
LIMIT 10 OFFSET 0;
-- 对应索引
KEY `idx_users_enable_deleted_created` (`enable`, `deleted_at`, `created_at`)
b.操作日志查询优化
-- 按用户和时间范围查询
SELECT * FROM sys_operation_records
WHERE user_id = 1
AND created_at BETWEEN '2023-01-01' AND '2023-12-31'
AND deleted_at IS NULL
ORDER BY created_at DESC;
-- 对应索引
KEY `idx_operation_user_time_deleted` (`user_id`, `created_at`, `deleted_at`)
c.权限验证查询优化
-- Casbin权限验证查询
SELECT * FROM casbin_rule
WHERE ptype = 'p'
AND v0 = '888'
AND v1 = '/user/getUserList'
AND v2 = 'POST';
-- 已有唯一索引覆盖此查询
UNIQUE KEY `idx_casbin_rule` (`ptype`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`)
04.数据库配置
a.GORM 配置
// config/gorm.go
type My struct {
GeneralDB `yaml:",inline" mapstructure:",squash"`
}
type GeneralDB struct {
Path string `mapstructure:"path" json:"path" yaml:"path"`
Port string `mapstructure:"port" json:"port" yaml:"port"`
Config string `mapstructure:"config" json:"config" yaml:"config"`
Dbname string `mapstructure:"db-name" json:"db-name" yaml:"db-name"`
Username string `mapstructure:"username" json:"username" yaml:"username"`
Password string `mapstructure:"password" json:"password" yaml:"password"`
Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"`
Singular bool `mapstructure:"singular" json:"singular" yaml:"singular"`
Engine string `mapstructure:"engine" json:"engine" yaml:"engine" default:"InnoDB"`
MaxIdleConns int `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns"`
MaxOpenConns int `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns"`
LogMode string `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode"`
LogZap bool `mapstructure:"log-zap" json:"log-zap" yaml:"log-zap"`
}
b.连接池配置
// initialize/gorm.go
func GormMy () *gorm.DB {
m := global.GVA_CONFIG.My
if m.Dbname == "" {
return nil
}
my Config := my .Config{
DSN: m.Dsn(),
DefaultStringSize: 191,
SkipInitializeWithVersion: false,
}
if db, err := gorm.Open(my .New(my Config), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil {
return nil
} else {
db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine)
DB, _ := db.DB()
DB.SetMaxIdleConns(m.MaxIdleConns)
DB.SetMaxOpenConns(m.MaxOpenConns)
return db
}
}
c.数据库迁移
// initialize/gorm.go
func RegisterTables() {
db := global.GVA_DB
err := db.AutoMigrate(
// 系统模块
system.SysUser{},
system.SysAuthority{},
system.SysApi{},
system.SysBaseMenu{},
system.SysBaseMenuBtn{},
system.SysBaseMenuParameter{},
system.SysAutoCode{},
system.SysAutoCodeHistory{},
system.SysDictionary{},
system.SysDictionaryDetail{},
system.SysOperationRecord{},
// 示例模块
example.ExaFile{},
example.ExaFileChunk{},
example.ExaFileUploadAndDownload{},
example.ExaCustomer{},
)
if err != nil {
global.GVA_LOG.Error("register table failed", zap.Error(err))
os.Exit(0)
}
global.GVA_LOG.Info("register table success")
}
05.数据库维护
a.备份策略
a.全量备份脚本
#!/bin/bash
# backup_full.sh
DB_NAME="gin_vue_admin"
DB_USER="root"
DB_PASS="password"
BACKUP_DIR="/backup/my "
DATE=$(date +"%Y%m%d_%H%M%S")
# 创建备份目录
mkdir -p $BACKUP_DIR
# 全量备份
my dump -u$DB_USER -p$DB_PASS \
--single-transaction \
--routines \
--triggers \
--events \
--hex-blob \
$DB_NAME > $BACKUP_DIR/full_backup_$DATE.
# 压缩备份文件
gzip $BACKUP_DIR/full_backup_$DATE.
# 删除7天前的备份
find $BACKUP_DIR -name "full_backup_*. .gz" -mtime +7 -delete
echo "Full backup completed: full_backup_$DATE. .gz"
b.增量备份脚本
#!/bin/bash
# backup_incremental.sh
DB_NAME="gin_vue_admin"
DB_USER="root"
DB_PASS="password"
BACKUP_DIR="/backup/my /incremental"
DATE=$(date +"%Y%m%d_%H%M%S")
# 创建备份目录
mkdir -p $BACKUP_DIR
# 增量备份(基于binlog)
my binlog --start-datetime="$(date -d '1 hour ago' '+%Y-%m-%d %H:%M:%S')" \
--stop-datetime="$(date '+%Y-%m-%d %H:%M:%S')" \
/var/lib/my /my -bin.* > $BACKUP_DIR/incremental_$DATE.
# 压缩备份文件
gzip $BACKUP_DIR/incremental_$DATE.
echo "Incremental backup completed: incremental_$DATE. .gz"
b.性能监控
a.慢查询监控
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- 查看慢查询统计
SELECT
SCHEMA_NAME,
DIGEST_TEXT,
COUNT_STAR,
AVG_TIMER_WAIT/1000000000 AS avg_time_seconds,
MAX_TIMER_WAIT/1000000000 AS max_time_seconds
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = 'gin_vue_admin'
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;
b.索引使用情况
-- 查看未使用的索引
SELECT
t.TABLE_SCHEMA,
t.TABLE_NAME,
t.INDEX_NAME,
t.NON_UNIQUE,
t.COLUMN_NAME
FROM information_schema.STATISTICS t
LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage i
ON t.TABLE_SCHEMA = i.OBJECT_SCHEMA
AND t.TABLE_NAME = i.OBJECT_NAME
AND t.INDEX_NAME = i.INDEX_NAME
WHERE t.TABLE_SCHEMA = 'gin_vue_admin'
AND i.INDEX_NAME IS NULL
AND t.INDEX_NAME != 'PRIMARY'
ORDER BY t.TABLE_NAME, t.INDEX_NAME;
-- 查看索引使用频率
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME,
COUNT_READ,
COUNT_WRITE,
COUNT_FETCH,
COUNT_INSERT,
COUNT_UPDATE,
COUNT_DELETE
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'gin_vue_admin'
ORDER BY COUNT_READ DESC;
c.数据清理
a.操作日志清理
-- 清理30天前的操作日志
DELETE FROM sys_operation_records
WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
-- 清理软删除的数据(谨慎操作)
DELETE FROM sys_users
WHERE deleted_at IS NOT NULL
AND deleted_at < DATE_SUB(NOW(), INTERVAL 90 DAY);
b.定期维护脚本
-- 优化表
OPTIMIZE TABLE sys_operation_records;
OPTIMIZE TABLE sys_users;
OPTIMIZE TABLE casbin_rule;
-- 分析表
ANALYZE TABLE sys_operation_records;
ANALYZE TABLE sys_users;
ANALYZE TABLE casbin_rule;
-- 检查表
CHECK TABLE sys_operation_records;
CHECK TABLE sys_users;
CHECK TABLE casbin_rule;
06.数据字典
a.状态码定义
字段 值 说明
enable 1 用户启用
enable 2 用户冻结
status 1 字典启用
status 0 字典禁用
hidden true 菜单隐藏
hidden false 菜单显示
b.默认数据
a.默认角色
INSERT INTO `sys_authorities` VALUES
('2023-01-01 00:00:00.000','2023-01-01 00:00:00.000',NULL,888,'普通用户',0,'dashboard'),
('2023-01-01 00:00:00.000','2023-01-01 00:00:00.000',NULL,8881,'普通用户子角色',888,'dashboard'),
('2023-01-01 00:00:00.000','2023-01-01 00:00:00.000',NULL,9528,'测试角色',0,'dashboard');
b.默认用户
INSERT INTO `sys_users` VALUES
(1,'2023-01-01 00:00:00.000','2023-01-01 00:00:00.000',NULL,'a303176530-3dda-4a33-b261-61809d378a34','admin','$2a$10$2XLuViXqLcn18zyGHU.9COFYqI16yGHjs6pXU5klUkdOKWjjceYUC','超级管理员','dark','https://qmplusimg.henrongyi.top/gva_header.jpg','#fff','#1890ff',888,'17611111111','[email protected]',1);
5 部署指南
5.1 项目上线
01.前端
在web目录下执行 npm run build 得到 dist文件夹 将dist文件夹上传到服务器 建议使用nginx进行代理 并且设置 proxy 把请求代理到后端
02.后端
a.在Windows 编译到 Linux 和Mac
# 交叉编译到Linux
set GOOS=linux
set GOARCH=amd64
go build -o app-linux
# 交叉编译到Mac
set GOOS=darwin
set GOARCH=amd64
go build -o app-mac
b.在Linux下交叉编译到Windows和Mac
# 交叉编译到Windows
GOOS=windows GOARCH=amd64 go build -o app-windows.exe
# 交叉编译到Mac
GOOS=darwin GOARCH=amd64 go build -o app-mac
c.在Mac下交叉编译到Windows和Linux
# 交叉编译到Windows
GOOS=windows GOARCH=amd64 go build -o app-windows.exe
# 交叉编译到Linux
GOOS=linux GOARCH=amd64 go build -o app-linux
d.说明
在 server下 go build . 得到一个可执行文件然后将可执行文件和config.yaml 以及 resource 文件夹上传至服务器
三者最好放在同一路径下,最终服务器目录结构可能如下
├── breakpointDir // 后续断点续传自动生成
├── chunk // 后续断点续传自动生成
├── fileDir // 后续断点续传自动生成
├── finish // 后续断点续传自动生成
├── resource
│ └── 子目录文件
├── dist
│ └── 子目录文件
├── gin-vue-admin
├── config.yaml
03.Nginx的配置(以下内容节取自授权文档)
a.安装Nginx
yum install -y nginx
#安装所有模块
yum -y install nginx-all-modules.noarch
# 启动nginx
systemctl start nginx && systemctl enable nginx
b.打开编辑配置文件
vim /etc/nginx/nginx.conf
c.参考配置代码如下
user root;
events {
worker_connections 1024; ## Default: 1024
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
index index.php index.html index.htm default.php default.htm default.html;
server_name home.mychat.cloud;
root 你的dist所在位置;
location /api {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:8888; # 设置代理服务器的协议和地址 端口要和后端部署保持一致
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
d.重启生效
sudo systemctl restart nginx
后端地址未作修改时默认为http://127.0.0.1:8888
5.2 docker
01.web前端项目单独打包
a.使用 nginx 镜像,my.conf 来源于 gin-vue-admin 的my.conf
server {
listen 8080;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /usr/share/nginx/html;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
try_files $uri $uri/ /index.html;
}
location /api {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
rewrite ^/api/(.*)$ /$1 break; #重写
proxy_pass http://127.0.0.1:8888; # 设置代理服务器的协议和地址
}
location /api/swagger/index.html {
proxy_pass http://127.0.0.1:8888/swagger/index.html;
}
}
b.Dockerfile 来源于 gin-vue-admin 的dockerfile_web
# 声明镜像来源为node:12.16.1
FROM node:12.16.1
# 声明工作目录
WORKDIR /gva_web/
# 拷贝整个web项目到当前工作目录
COPY . .
# 通过npm下载cnpm
RUN npm install -g cnpm --registry=https://registry.npm.taobao.org
# 使用cnpm进行安装依赖
RUN cnpm install || npm install
# 使用npm run build命令打包web项目
RUN npm run build
# ===================================================== 以下为多阶段构建 ==========================================================
# 声明镜像来源为nginx:alpine, alpine 镜像小
FROM nginx:alpine
# 镜像编写者及邮箱
LABEL MAINTAINER="SliverHorn@[email protected]"
# 从.docker-compose/nginx/conf.d/目录拷贝my.conf到容器内的/etc/nginx/conf.d/my.conf
COPY .docker-compose/nginx/conf.d/my.conf /etc/nginx/conf.d/my.conf
# 从第一阶段进行拷贝文件
COPY --from=0 /gva_web/dist /usr/share/nginx/html
# 查看/etc/nginx/nginx.conf文件
RUN cat /etc/nginx/nginx.conf
# 查看 /etc/nginx/conf.d/my.conf
RUN cat /etc/nginx/conf.d/my.conf
# 查看 文件是否拷贝成功
RUN ls -al /usr/share/nginx/html
02.server项目单独打包
a.Dockerfile 来源于 gin-vue-admin 的 Dockerfile
# 声明镜像来源为golang:alpine
FROM golang:alpine
# 声明工作目录
WORKDIR /go/src/gin-vue-admin
# 拷贝整个server项目到工作目录
COPY . .
# go generate 编译前自动执行代码
# go env 查看go的环境变量
# go build -o server . 打包项目生成文件名为server的二进制文件
RUN go generate && go env && go build -o server .
# ==================================================== 以下为多阶段构建 ==========================================================
# 声明镜像来源为alpine:latest
FROM alpine:latest
# 镜像编写者及邮箱
LABEL MAINTAINER="SliverHorn@[email protected]"
# 声明工作目录
WORKDIR /go/src/gin-vue-admin
# 把/go/src/gin-vue-admin整个文件夹的文件到当前工作目录
COPY --from=0 /go/src/gin-vue-admin ./
EXPOSE 8888
# 运行打包好的二进制 并用-c 指定config.docker.yaml配置文件
ENTRYPOINT ./server -c config.docker.yaml
03.根据Dockerfile生成Docker镜像
# -d 后台运行
# -p 映射端口:内部端口
# -name 容器名字
# gva-server:1.0为docker build时的-t的参数
docker run -d -p 8888:8888 --name gva-server-v1 gva-server:1.0
# -it 以可交互模式运行并进入容器, 使用快捷键Ctrl + p + q即后台运行程序,Ctrl+c为退出容器
# -p 映射端口:内部端口
# -name 容器名字
# gva-server:1.0为docker build时的-t的参数
docker run -it -p 8888:8888 --name gva-server-v1 gva-server:1.0
5.3 docker-compose
01.Web使用Docker打包示例
a.使用 nginx 镜像,my.conf 来源于 gin-vue-admin 的my.conf
server {
listen 8080;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /usr/share/nginx/html;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
try_files $uri $uri/ /index.html;
}
location /api {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
rewrite ^/api/(.*)$ /$1 break; #重写
proxy_pass http://177.7.0.12:8888; # 设置代理服务器的协议和地址
}
location /api/swagger/index.html {
proxy_pass http://127.0.0.1:8888/swagger/index.html;
}
}
b.Dockerfile 来源于 gin-vue-admin 的Dockerfile
# 声明镜像来源为node:12.16.1
FROM node:12.16.1
# 声明工作目录
WORKDIR /gva_web/
# 拷贝整个web项目到当前工作目录
COPY . .
# 通过npm下载cnpm
RUN npm install -g cnpm --registry=https://registry.npm.taobao.org
# 使用cnpm进行安装依赖
RUN cnpm install || npm install
# 使用npm run build命令打包web项目
RUN npm run build
# ===================================================== 以下为多阶段构建 ==========================================================
# 声明镜像来源为nginx:alpine, alpine 镜像小
FROM nginx:alpine
# 镜像编写者及邮箱
LABEL MAINTAINER="SliverHorn@[email protected]"
# 从.docker-compose/nginx/conf.d/目录拷贝my.conf到容器内的/etc/nginx/conf.d/my.conf
COPY .docker-compose/nginx/conf.d/my.conf /etc/nginx/conf.d/my.conf
# 从第一阶段进行拷贝文件
COPY --from=0 /gva_web/dist /usr/share/nginx/html
# 查看/etc/nginx/nginx.conf文件
RUN cat /etc/nginx/nginx.conf
# 查看 /etc/nginx/conf.d/my.conf
RUN cat /etc/nginx/conf.d/my.conf
# 查看 文件是否拷贝成功
RUN ls -al /usr/share/nginx/html
02.Server使用Docker打包示例
a.说明
mysql -> path 的 mysql 会自动获取mysql服务的容器内部ip及端口
redis -> path 的 redis 会自动获取redis服务的容器内部ip
b.Dockerfile 来源于 gin-vue-admin 的 Dockerfile
# 声明镜像来源为golang:alpine
FROM golang:alpine
# 声明工作目录
WORKDIR /go/src/gin-vue-admin
# 拷贝整个server项目到工作目录
COPY . .
# go generate 编译前自动执行代码
# go env 查看go的环境变量
# go build -o server . 打包项目生成文件名为server的二进制文件
RUN go generate && go env && go build -o server .
# ==================================================== 以下为多阶段构建 ==========================================================
# 声明镜像来源为alpine:latest
FROM alpine:latest
# 镜像编写者及邮箱
LABEL MAINTAINER="SliverHorn@[email protected]"
# 声明工作目录
WORKDIR /go/src/gin-vue-admin
# 把/go/src/gin-vue-admin中的可执行文件以及配置文件(resource模板文件)添加进入docker中
COPY --from=0 /go/src/gin-vue-admin/server ./
COPY --from=0 /go/src/gin-vue-admin/config.docker.yaml ./
COPY --from=0 /go/src/gin-vue-admin/resource ./
EXPOSE 8888
# 运行打包好的二进制 并用-c 指定config.docker.yaml配置文件
ENTRYPOINT ./server -c config.docker.yaml
03.docker-compose.yaml详解
a.docker-compose.yaml 来源于 gin-vue-admin 的 docker-compose.yaml
version: "3"
# 声明一个名为network的networks,subnet为network的子网地址,默认网关是177.7.0.1
networks:
network:
ipam:
driver: default
config:
- subnet: '177.7.0.0/16'
# 设置mysql,redis持久化保存
volumes:
mysql:
services:
# web服务
web:
build:
context: ./web
# 指定dockerfile启动容器
dockerfile: ./Dockerfile
# 自定义容器名
container_name: gva-web
# 容器启动失败是否重启
restart: always
# 映射端口
ports:
- '8080:8080'
# web服务依赖于server服务
depends_on:
- server
command: [ 'nginx-debug', '-g', 'daemon off;' ]
networks:
network:
# 在network网络下的容器内部的Ipv4地址
ipv4_address: 177.7.0.11
# server服务
server:
build:
context: ./server
# 指定dockerfile启动容器
dockerfile: ./Dockerfile
# 自定义容器名
container_name: gva-server
# 容器启动失败是否重启
restart: always
# 映射端口
ports:
- '8888:8888'
# server服务依赖于mysql服务于redis服务
depends_on:
- mysql
- redis
networks:
network:
# 在network网络下的容器内部的Ipv4地址
ipv4_address: 177.7.0.12
mysql:
# 指定mysql镜像版本
# 如果您是 arm64 架构:如 MacOS 的 M1,请修改镜像为 image: mysql/mysql-server:8.0.21
image: mysql:8.0.21
# 自定义容器名
container_name: gva-mysql
# 设置utf8字符集
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
# 容器启动失败是否重启
restart: always
# 映射端口
ports:
- "13306:3306" # host物理直接映射端口为13306
# 系统环境变量
environment:
MYSQL_DATABASE: 'qmPlus' # 初始化启动时要创建的数据库的名称
MYSQL_ROOT_PASSWORD: 'Aa@6447985' # root管理员用户密码
# 映射数据卷到数据库
volumes:
- mysql:/var/lib/mysql
networks:
network:
# 在network网络下的容器内部的Ipv4地址
ipv4_address: 177.7.0.13
# redis服务
redis:
# 指定redis镜像版本
image: redis:6.0.6
# 自定义容器名
container_name: gva-redis # 容器名
# 容器启动失败是否重启
restart: always
# 映射端口
ports:
- '6379:6379'
networks:
network:
# 在network网络下的容器内部的Ipv4地址
ipv4_address: 177.7.0.14
04.常用命令
# 使用docker-compose启动四个容器
docker-compose -f deploy/docker-compose/docker-compose.yaml up
# 如果您修改了某些配置选项,可以使用此命令重新打包镜像
docker-compose -f deploy/docker-compose/docker-compose.yaml up --build
# 使用docker-compose 后台启动
docker-compose -f deploy/docker-compose/docker-compose.yaml up -d
# 使用docker-compose 重新打包镜像并后台启动
docker-compose -f deploy/docker-compose/docker-compose.yaml up --build -d
# 服务都启动成功后,使用此命令行可清除none镜像
docker system prune
5.4 kubernetes
01.使用声明
上云需要手动初始化数据库,不支持在线初始化操作 (/deployment/server/gva-server-configmap.yaml)
自定义初始化数据库后,将配置写入/deployment/server/gva-server-configmap.yaml
02.选择访问方式
a.域名访问
如:http://demo.gin-vue-admin.com
修改代码第9行为自己的域名 deploy/kubernetes/web/gva-web-ingress.yaml
b.service访问
如:http://127.0.0.1:30180
修改代码第12行以后为如下配置即可 deploy/kubernetes/web/gva-web-service.yaml
spec:
type: NodePort
ports:
- name: http
port: 8080
targetPort: 8080
nodePort: 30180
selector:
app: gva-web
version: gva-vue3
03.选择镜像
a.使用gin-vue-admin官方镜像
前端如:image: registry.cn-hangzhou.aliyuncs.com/gva/web:latest
修改代码第26行为需要部署的镜像 deploy/kubernetes/web/gva-web-deploymemt.yaml
-----------------------------------------------------------------------------------------------------
后端如:image: registry.cn-hangzhou.aliyuncs.com/gva/server:latest
修改代码第26行为需要部署的镜像 deploy/kubernetes/server/gva-server-deployment.yaml
b.使用自定义镜像
可参考docker页面制作自定义镜像,上传至镜像仓库后,按照步骤一修改yaml文件即可。
04.开始部署
# 使用默认namespace
kubectl apply -f deploy/kubernetes/server/ -f deploy/kubernetes/web/
# 指定namespace
kubectl apply -f deploy/kubernetes/server/ -f deploy/kubernetes/web/ -n namespace
# 重启server服务
kubectl -n namespace rollout restart deployment gva-server
# 扩容server服务
kubectl -n namespace scale deployment gva-server --replicas 2
# 清除gva服务
kubectl delete -f deploy/kubernetes/server/ -f deploy/kubernetes/web/ -n namespace