跟着官方学习 GVM 脚手架一般用法
记录下在跟着官方教程学习 GVM 的 auto-code 生成器时,项目的代码结构。
注意:
- 当前 GVM 版本为 v2.8.2
- 不是说明项目的所有代码结构,仅仅是关于实际开发的时候会经常接触到的代码结构。
自动代码模板配置
在后台菜单 系统工具 -> 模板配置 中可以配置项目的模板。

在这里我填写的模板名称是 demo ,选择的是包模板。
填写完相关信息后,观察 Git 仓库的代码结构,会发现新建了三个文件夹,以及修改了三处文件。
新增部分代码
- 新增了 api/v1/demo 文件夹,用于存放参数校验,返回响应相关的代码(控制器层)。
- 新增了 service/demo 文件夹,用于存放逻辑处理相关的代码(服务层)。
- 新增了 router/demo 文件夹,用于存放路由相关的代码(路由层)。
思考
新增的这些文件夹的包名都是 demo ,他们之间互相引用是否需要导包了呢?
答案1
在每个新增的文件夹中,都新增了一个 enter.go 文件,相当于模块的入口文件。
默认生成的 enter.go 文件内容如下:
go
// router/demo/enter.go
package demo
type RouterGroup struct {}
// api/v1/demo/enter.go
package demo
type ApiGroup struct {}
// service/demo/enter.go
package demo
type ServiceGroup struct {}
值得注意的是 RouterGroup
ApiGroup
ServiceGroup
这三个结构体命名是固定写法。
修改部分代码

如果所示,修改的部分就是在对应的新建包的上级包 enter.go 文件,添加对结构体的嵌入引入。
修改了文件:router/enter.go api/v1/enter.go service/enter.go
代码自动生成配置
代码自动生成配置需要依赖于自动代码模板配置。
我填写的配置参考如下:
自动化结构配置:

说明:
MyDemo:
- 这里配置的结构体名称 MyDemo 会是将来自动生成代码后端的结构体名称。
- 前端定义的接口名称会是 MyDemo ,但是会在前面添加前缀
createMyDemo
deleteMyDemo
updateMyDemo
findMyDemo
getMyDemoList
等。
mdo:
- 后端的一些变量的简称。如:
var mdoApi = api.ApiGroupApp.DemoApiGroup.MyDemoApi
中文名称:
- 一些注释会使用到中文名称。
- 后台菜单名称会用这个中文名称。
表名:
- 数据库表名会用这个名称。
文件名称:
- 后端的文件名称会用这个名称。
- 前端的文件名称会用这个名称。
结构体字段配置(数据库字段):



前端
新增部分代码
-
在 src/api/demo/my-demos.js 下新增了该结构的 CRUD 一般方法。
-
在 src/view/demo/my-demos 文件夹下新增了 my-demos.vue 和 my-demosForm.vue 两个文件。
my-demos.vue 是一个完整的页面,包含了列表、新增、修改、删除等功能。
my-demosForm.vue 是新增的表单页面。
修改部分代码
会修改 src/pathInfo.json 文件,添加路由信息。
"/src/view/demo/my-demos/my-demos.vue": "MyDemo",
"/src/view/demo/my-demos/my-demosForm.vue": "MyDemoForm"
后端
路由入口
- 新增了文件 router/demo/my-demos.go
go
package demo
import (
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
"github.com/gin-gonic/gin"
)
type MyDemoRouter struct {}
// InitMyDemoRouter 初始化 我的测试demo 路由信息
func (s *MyDemoRouter) InitMyDemoRouter(Router *gin.RouterGroup,PublicRouter *gin.RouterGroup) {
mdoRouter := Router.Group("mdo").Use(middleware.OperationRecord())
mdoRouterWithoutRecord := Router.Group("mdo")
mdoRouterWithoutAuth := PublicRouter.Group("mdo")
{
mdoRouter.POST("createMyDemo", mdoApi.CreateMyDemo) // 新建我的测试demo
mdoRouter.DELETE("deleteMyDemo", mdoApi.DeleteMyDemo) // 删除我的测试demo
mdoRouter.DELETE("deleteMyDemoByIds", mdoApi.DeleteMyDemoByIds) // 批量删除我的测试demo
mdoRouter.PUT("updateMyDemo", mdoApi.UpdateMyDemo) // 更新我的测试demo
}
{
mdoRouterWithoutRecord.GET("findMyDemo", mdoApi.FindMyDemo) // 根据ID获取我的测试demo
mdoRouterWithoutRecord.GET("getMyDemoList", mdoApi.GetMyDemoList) // 获取我的测试demo列表
}
{
mdoRouterWithoutAuth.GET("getMyDemoPublic", mdoApi.GetMyDemoPublic) // 我的测试demo开放接口
}
}
- 将方法
InitMyDemoRouter
在 initialize/router_biz.go 中引入。
go
package initialize
import (
"github.com/flipped-aurora/gin-vue-admin/server/router"
"github.com/gin-gonic/gin"
)
func holder(routers ...*gin.RouterGroup) {
_ = routers
_ = router.RouterGroupApp
}
func initBizRouter(routers ...*gin.RouterGroup) {
privateGroup := routers[0]
publicGroup := routers[1]
holder(publicGroup, privateGroup) // 占位方法,保证文件可以正确加载,避免go空变量检测报错,请勿删除。
{
demoRouter := router.RouterGroupApp.Demo
demoRouter.InitMyDemoRouter(privateGroup, publicGroup)
}
}
控制器层
- 新增了文件 api/v1/demo/my-demos.go
go
package demo
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/model/demo"
demoReq "github.com/flipped-aurora/gin-vue-admin/server/model/demo/request"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MyDemoApi struct {}
// CreateMyDemo 创建我的测试demo
// @Tags MyDemo
// @Summary 创建我的测试demo
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data body demo.MyDemo true "创建我的测试demo"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /mdo/createMyDemo [post]
func (mdoApi *MyDemoApi) CreateMyDemo(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
var mdo demo.MyDemo
err := c.ShouldBindJSON(&mdo)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
err = mdoService.CreateMyDemo(ctx,&mdo)
if err != nil {
global.GVA_LOG.Error("创建失败!", zap.Error(err))
response.FailWithMessage("创建失败:" + err.Error(), c)
return
}
response.OkWithMessage("创建成功", c)
}
// DeleteMyDemo 删除我的测试demo
// @Tags MyDemo
// @Summary 删除我的测试demo
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data body demo.MyDemo true "删除我的测试demo"
// @Success 200 {object} response.Response{msg=string} "删除成功"
// @Router /mdo/deleteMyDemo [delete]
func (mdoApi *MyDemoApi) DeleteMyDemo(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
ID := c.Query("ID")
err := mdoService.DeleteMyDemo(ctx,ID)
if err != nil {
global.GVA_LOG.Error("删除失败!", zap.Error(err))
response.FailWithMessage("删除失败:" + err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
// DeleteMyDemoByIds 批量删除我的测试demo
// @Tags MyDemo
// @Summary 批量删除我的测试demo
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{msg=string} "批量删除成功"
// @Router /mdo/deleteMyDemoByIds [delete]
func (mdoApi *MyDemoApi) DeleteMyDemoByIds(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
IDs := c.QueryArray("IDs[]")
err := mdoService.DeleteMyDemoByIds(ctx,IDs)
if err != nil {
global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
response.FailWithMessage("批量删除失败:" + err.Error(), c)
return
}
response.OkWithMessage("批量删除成功", c)
}
// UpdateMyDemo 更新我的测试demo
// @Tags MyDemo
// @Summary 更新我的测试demo
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data body demo.MyDemo true "更新我的测试demo"
// @Success 200 {object} response.Response{msg=string} "更新成功"
// @Router /mdo/updateMyDemo [put]
func (mdoApi *MyDemoApi) UpdateMyDemo(c *gin.Context) {
// 从ctx获取标准context进行业务行为
ctx := c.Request.Context()
var mdo demo.MyDemo
err := c.ShouldBindJSON(&mdo)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
err = mdoService.UpdateMyDemo(ctx,mdo)
if err != nil {
global.GVA_LOG.Error("更新失败!", zap.Error(err))
response.FailWithMessage("更新失败:" + err.Error(), c)
return
}
response.OkWithMessage("更新成功", c)
}
// FindMyDemo 用id查询我的测试demo
// @Tags MyDemo
// @Summary 用id查询我的测试demo
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param ID query uint true "用id查询我的测试demo"
// @Success 200 {object} response.Response{data=demo.MyDemo,msg=string} "查询成功"
// @Router /mdo/findMyDemo [get]
func (mdoApi *MyDemoApi) FindMyDemo(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
ID := c.Query("ID")
remdo, err := mdoService.GetMyDemo(ctx,ID)
if err != nil {
global.GVA_LOG.Error("查询失败!", zap.Error(err))
response.FailWithMessage("查询失败:" + err.Error(), c)
return
}
response.OkWithData(remdo, c)
}
// GetMyDemoList 分页获取我的测试demo列表
// @Tags MyDemo
// @Summary 分页获取我的测试demo列表
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data query demoReq.MyDemoSearch true "分页获取我的测试demo列表"
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
// @Router /mdo/getMyDemoList [get]
func (mdoApi *MyDemoApi) GetMyDemoList(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
var pageInfo demoReq.MyDemoSearch
err := c.ShouldBindQuery(&pageInfo)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
list, total, err := mdoService.GetMyDemoInfoList(ctx,pageInfo)
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败:" + err.Error(), c)
return
}
response.OkWithDetailed(response.PageResult{
List: list,
Total: total,
Page: pageInfo.Page,
PageSize: pageInfo.PageSize,
}, "获取成功", c)
}
// GetMyDemoPublic 不需要鉴权的我的测试demo接口
// @Tags MyDemo
// @Summary 不需要鉴权的我的测试demo接口
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /mdo/getMyDemoPublic [get]
func (mdoApi *MyDemoApi) GetMyDemoPublic(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
// 此接口不需要鉴权
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
mdoService.GetMyDemoPublic(ctx)
response.OkWithDetailed(gin.H{
"info": "不需要鉴权的我的测试demo接口信息",
}, "获取成功", c)
}
- 将控制层的结构体
MyDemoApi
在包的入口文件 api/v1/demo/enter.go 中引入,同时引入相关的服务层。
go
package demo
import "github.com/flipped-aurora/gin-vue-admin/server/service"
type ApiGroup struct{ MyDemoApi }
var mdoService = service.ServiceGroupApp.DemoServiceGroup.MyDemoService
- 在 model/demo/request/my-demos.go 中创建请求参数结构体
MyDemoSearch
。
go
package request
import (
"github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"time"
)
type MyDemoSearch struct{
CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"`
Name *string `json:"name" form:"name"`
Age *int `json:"age" form:"age"`
Desc *string `json:"desc" form:"desc"`
request.PageInfo
}
服务层
- 新增文件 service/demo/my-demos.go ,在文件中新增
MyDemoService
结构体。
go
package demo
import (
"context"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/demo"
demoReq "github.com/flipped-aurora/gin-vue-admin/server/model/demo/request"
)
type MyDemoService struct {}
// CreateMyDemo 创建我的测试demo记录
// Author [yourname](https://github.com/yourname)
func (mdoService *MyDemoService) CreateMyDemo(ctx context.Context, mdo *demo.MyDemo) (err error) {
err = global.GVA_DB.Create(mdo).Error
return err
}
// DeleteMyDemo 删除我的测试demo记录
// Author [yourname](https://github.com/yourname)
func (mdoService *MyDemoService)DeleteMyDemo(ctx context.Context, ID string) (err error) {
err = global.GVA_DB.Delete(&demo.MyDemo{},"id = ?",ID).Error
return err
}
// DeleteMyDemoByIds 批量删除我的测试demo记录
// Author [yourname](https://github.com/yourname)
func (mdoService *MyDemoService)DeleteMyDemoByIds(ctx context.Context, IDs []string) (err error) {
err = global.GVA_DB.Delete(&[]demo.MyDemo{},"id in ?",IDs).Error
return err
}
// UpdateMyDemo 更新我的测试demo记录
// Author [yourname](https://github.com/yourname)
func (mdoService *MyDemoService)UpdateMyDemo(ctx context.Context, mdo demo.MyDemo) (err error) {
err = global.GVA_DB.Model(&demo.MyDemo{}).Where("id = ?",mdo.ID).Updates(&mdo).Error
return err
}
// GetMyDemo 根据ID获取我的测试demo记录
// Author [yourname](https://github.com/yourname)
func (mdoService *MyDemoService)GetMyDemo(ctx context.Context, ID string) (mdo demo.MyDemo, err error) {
err = global.GVA_DB.Where("id = ?", ID).First(&mdo).Error
return
}
// GetMyDemoInfoList 分页获取我的测试demo记录
// Author [yourname](https://github.com/yourname)
func (mdoService *MyDemoService)GetMyDemoInfoList(ctx context.Context, info demoReq.MyDemoSearch) (list []demo.MyDemo, total int64, err error) {
limit := info.PageSize
offset := info.PageSize * (info.Page - 1)
// 创建db
db := global.GVA_DB.Model(&demo.MyDemo{})
var mdos []demo.MyDemo
// 如果有条件搜索 下方会自动创建搜索语句
if len(info.CreatedAtRange) == 2 {
db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1])
}
if info.Name != nil && *info.Name != "" {
db = db.Where("name = ?", *info.Name)
}
if info.Age != nil {
db = db.Where("age = ?", *info.Age)
}
if info.Desc != nil && *info.Desc != "" {
db = db.Where("desc = ?", *info.Desc)
}
err = db.Count(&total).Error
if err!=nil {
return
}
if limit != 0 {
db = db.Limit(limit).Offset(offset)
}
err = db.Find(&mdos).Error
return mdos, total, err
}
func (mdoService *MyDemoService)GetMyDemoPublic(ctx context.Context) {
// 此方法为获取数据源定义的数据
// 请自行实现
}
模型层
- 新增文件 model/demo/my-demos.go ,在文件中新增
MyDemo
结构体。
go
// 自动生成模板MyDemo
package demo
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
// 我的测试demo 结构体 MyDemo
type MyDemo struct {
global.GVA_MODEL
Name *string `json:"name" form:"name" gorm:"comment:姓名desc;column:name;size:20;"` //姓名zh
Age *int `json:"age" form:"age" gorm:"default:0;comment:年龄desc;column:age;size:120;"` //年龄zh
Desc *string `json:"desc" form:"desc" gorm:"comment:描述desc;column:desc;size:255;"` //描述zh
}
// TableName 我的测试demo MyDemo自定义表名 my_dmos
func (MyDemo) TableName() string {
return "my_dmos"
}
- 在之前预留的模型注册文件 initialize/gorm_biz.go 中新增
MyDemo
模型。
go
package initialize
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/demo"
)
func bizModel() error {
db := global.GVA_DB
// 确保所有已注册模型(包括插件模型)都完成迁移
err := db.AutoMigrate(demo.MyDemo{})
if err != nil {
return err
}
return nil
}
手动创建前端页面
一级页面
- 在前端代码 src 文件夹下书写相关的前端代码。
vue
<!-- src/views/myPage/index.vue -->
<template>
<div>我的1级页面</div>
</template>
<script setup lang="ts"></script>
<style></style>
- 在后台 超级管理员 -> 菜单管理 页面配置菜单。

- 在后台 超级管理员 -> 角色管理 页面给相应的角色分配菜单。

- 刷新页面,即可看见新增的菜单。

二级页面
- 在后台 超级管理员 -> 菜单管理 页面配置菜单。

注意这里的路由 Name 值不能重复。
- 修改需要作为二级页面的子页面的菜单编辑,将 父节点 ID 选择为新增的一级菜单。

- 在后台 超级管理员 -> 角色管理 页面给相应的角色重新分配分配菜单。

针对当前的菜单页面,在这里需要取消勾选再重新勾选。
- 刷新页面,即可看见新增的二级菜单。

上面的方法是采用使用 GVM 预设的 src/view/routerHolder.vue 二级页面框架。
如果需要定制的话也可以复制该文件到自己的文件夹中。在菜单配置中选择自己的文件。
按钮权限
- 在后台 超级管理员 -> 菜单管理 页面选择相应的页面配置可控按钮。

- 在后台 超级管理员 -> 角色管理 页面为相应的角色分配按钮权限。

- 在前端代码中使用按钮权限。
vue
<template>
<div>我的1级页面</div>
<span v-auth="btnAuth.a"> 权限a控制 </span>
<span v-auth="btnAuth.b"> 权限b控制 </span>
</template>
<script setup>
import { useBtnAuth } from "@/utils/btnAuth";
const btnAuth = useBtnAuth();
console.log(btnAuth, btnAuth.a, btnAuth.b);
</script>
<style></style>
- 刷新页面,即可看见按钮权限生效。

还可以在代码中使用 v-auth.not
指令来判断取反。
字典
- 在后台 超级管理员 -> 字典管理 页面新增字典。

- 在新增的字典中新增字典数据。

- 在前端代码中使用字典数据。
vue
<template>
<div>我的1级页面</div>
<div v-for="item in cityDict" :key="item.value">
{{ item }}
</div>
{{ showDictLabel(cityDict, "beijing") }}
</template>
<script setup>
import { getDict, showDictLabel } from "@/utils/dictionary";
import { ref, onMounted } from "vue";
const cityDict = ref([]);
onMounted(async () => {
cityDict.value = await getDict("citys");
});
</script>
<style></style>
- 刷新页面,即可看见字典数据生效。

-
还是需要导包的。 ↩︎