文章目录
- Softhub软件下载站实战开发(四):代码生成器设计与实现
- 1.前言 📜
- 2.技术选型
- 3.架构概览 🏗️
- 3.1 架构概览
- 3.2 工作流程详解
- 4.核心功能实现 ⏳
- 4.1 配置管理系统
- 4.2 数据库表结构解析
- 4.3 模板渲染引擎
- 4.4 智能类型转换
- 4.5 动态文件生成
- 4.6 智能覆盖策略
- 4.7 运行
- 5.附录 ℹ️
- 5.1 生成器代码
- 5.2 后端模板
- 5.3 前端模板
Softhub软件下载站实战开发(四):代码生成器设计与实现
1.前言 📜
在上篇文章中我们以platform
模块为例,详细记录了从创建model到编写页面的全过程。相信读者已经对编写流程有了一个大致的了解。
不难发现,整个项目分为多层:controller、service、dao等等,并且存在着大量模板代码,手动一个个文件编写费事费力,还容易出错。GoFrame其实为我们提供了cli工具,可以方便快捷的生成例如model、service等代码。
对整个项目而言,其实这还远远不够,我们还有例如controller、service、前端等模板代码需要编写,这时候编写一个简单的代码生成器就会方便很多。
2.技术选型
因为本项目代码生成器不是重点,因此不考虑项目中集成,仅是作为一个工具存在。因为方便快捷作为第一考虑因素。
因此我们选用python作为主语言、结合jinja模板引擎快速生成模板代码。
系统核心组件包括:
- 数据库元数据提取 - 通过SQLAlchemy分析表结构
- 模板引擎 - 使用Jinja2渲染代码模板
- 配置管理 - YAML配置文件支持
- 文件生成器 - 自动化文件创建和写入
3.架构概览 🏗️
3.1 架构概览
3.2 工作流程详解
4.核心功能实现 ⏳
4.1 配置管理系统
database:host: 127.0.0.1port: 3306user: rootpassword: 123456dbname: softhubcode_generator:# 生成类型:frontend-前端, backend-后端, all-全部generate_type: all# 表配置tables:- name: ds_platform # 表名comment: "平台管理" # 表注释generate_type: all # 可选:frontend, backend, allfrontend:template_name: "gfast前端模板2"output_path: "D:/output/frontend"primary_name: "name" # 主键名称backend:template_name: "gfast模板"output_path: "D:/output/backend"package: "platform" # 包名package_path: "github.com/tiger1103/gfast/v3" # 包路径前缀
配置模块管理
class ConfigUtil:def __init__(self, config_path: Path):self.config_path = config_pathdef read_config(self) -> Dict[str, Any]:with open(self.config_path, 'r', encoding='utf-8') as f:return yaml.safe_load(f)
使用YAML配置文件管理:
- 数据库连接信息
- 模板路径配置
- 包名和导入路径
- 代码生成选项
4.2 数据库表结构解析
class InspectUtils:def __init__(self, engine):self.engine = engineself.inspector = inspect(engine)def get_infos(self, table_name: str) -> Dict[str, Any]:columns = self.inspector.get_columns(table_name)# 处理字段信息...return {'table_name': table_name,'columns': columns,# 其他元数据...}
- 使用SQLAlchemy的Inspector获取表元数据
- 自动将SQL类型转换为Go类型
- 生成驼峰命名等符合编程规范的字段名
4.3 模板渲染引擎
# 设置模板路径
template_base_path = Path('template')
template_path = template_base_path.joinpath(template_name)# 渲染模板
templates = Environment(loader=FileSystemLoader(template_path), lstrip_blocks=True, trim_blocks=True)
tl = templates.get_template(template)
render_str = tl.render(infos)
- 支持模板继承和宏定义
- 自动处理缩进和空白字符
- 动态生成文件路径和文件名
4.4 智能类型转换
def _get_go_type(self, sql_type: str) -> str:"""将SQL类型转换为Go类型"""sql_type = sql_type.lower()if 'int' in sql_type:return 'int'elif 'varchar' in sql_type or 'char' in sql_type or 'text' in sql_type:return 'string'elif 'time' in sql_type or 'date' in sql_type:return '*gtime.Time'# 其他类型处理...
4.5 动态文件生成
# 渲染相对路径
relative_path = Template(str(item_template_path.parent.relative_to(template_path)).render(file_info)
# 渲染文件名
template_stem = item_template_path.stem
if '.' in template_stem:name_part, ext = template_stem.rsplit('.', 1)file_name = Template(name_part).render(file_info) + '.' + ext
4.6 智能覆盖策略
# 跳过已存在的router.go和logic.go文件
if Path(file_path).exists() and (str(item_template_path.stem) in ["router.go", "logic.go"]):continue
4.7 运行
修改config\application.yml
配置
python code_generator.py
输出类似如下
5.附录 ℹ️
5.1 生成器代码
import logging as log
import os
from datetime import datetime
from pathlib import Path
from typing import Dict, Any, Listfrom jinja2 import Environment, FileSystemLoader, Template
import sqlalchemy as sa
from sqlalchemy import inspect
import yaml# 配置日志
log.basicConfig(level=log.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')class ConfigUtil:def __init__(self, config_path: Path):self.config_path = config_pathdef read_config(self) -> Dict[str, Any]:with open(self.config_path, 'r', encoding='utf-8') as f:return yaml.safe_load(f)class InspectUtils:def __init__(self, engine):self.engine = engineself.inspector = inspect(engine)self.tables = self.inspector.get_table_names()def get_infos(self, table_name: str) -> Dict[str, Any]:columns = self.inspector.get_columns(table_name)primary_keys = self.inspector.get_pk_constraint(table_name)foreign_keys = self.inspector.get_foreign_keys(table_name)# 处理字段信息field_info = []for column in columns:# 获取字段类型type_name = str(column['type'])go_type = self._get_go_type(type_name)# 处理字段名name = column['name']camel_name = ''.join(word.capitalize() for word in name.split('_'))if camel_name and camel_name[0].isupper():camel_name = camel_name[0].lower() + camel_name[1:]field_info.append({'name': name,'camelName': camel_name,'firstUpperName': ''.join(word.capitalize() for word in name.split('_')),'type': go_type,'comment': column.get('comment', ''),'nullable': column.get('nullable', False)})return {'table_name': table_name,'columns': columns,'primary_keys': primary_keys,'foreign_keys': foreign_keys,'field_info': field_info}def _get_go_type(self, sql_type: str) -> str:"""将SQL类型转换为Go类型"""sql_type = sql_type.lower()if 'int' in sql_type:return 'int'elif 'varchar' in sql_type or 'char' in sql_type or 'text' in sql_type:return 'string'elif 'float' in sql_type or 'double' in sql_type or 'decimal' in sql_type:return 'float64'elif 'bool' in sql_type:return 'bool'elif 'time' in sql_type or 'date' in sql_type:return '*gtime.Time'else:return 'string'def get_file_info(self, table_name: str) -> Dict[str, str]:first_upper_name = ''.join(word.capitalize() for word in table_name.split('_'))return {'table_name': table_name,'table_name_camel': ''.join(word.capitalize() for word in table_name.split('_')),'table_name_lower': table_name.lower(),'name': table_name,'first_upper_name': first_upper_name}def get_connection(config: Dict[str, Any]):return sa.create_engine(f"mysql+pymysql://{config['user']}:{config['password']}@{config['host']}:{config['port']}/{config['dbname']}")def generate_code(config_map: Dict[str, Any], table_config: Dict[str, Any]):template_name = config_map['template_name']table_name = config_map['table_name']output_path = config_map['output_path']package = config_map.get('package', 'template')package_path = config_map.get('package_path', 'github.com/tiger1103/gfast/v3')# 读取配置config_path = Path('config/application.yml').resolve()config_util = ConfigUtil(config_path)config = config_util.read_config()# 连接数据库engine = get_connection(config['database'])iu = InspectUtils(engine)# 获取表信息infos = iu.get_infos(table_name)# 添加前端信息front_info = {'search_columns': [{"camelName": "name", "comment": "变量名称", "type": "string"}]}if "primary_name" in config_map:front_info['primaryName'] = config_map['primary_name']infos['front_info'] = front_info# 添加包信息package_info = {'api': f"{package_path}/api/v1/{package}",'service': f"{package_path}/internal/app/{package}/service",'dao': f"{package_path}/internal/app/{package}/dao",'liberr': f"{package_path}/library/liberr",'libUtils': f"{package_path}/library/libUtils",'consts': f"{package_path}/internal/app/system/consts",'model': f"{package_path}/internal/app/{package}/model",'entity': f"{package_path}/internal/app/{package}/model/entity",'utils': f"{package_path}/internal/app/{package}/utils",'common': f"{package_path}/internal/app/{package}/common",'commonDao': f"{package_path}/internal/app/common/dao",'commonEntity': f"{package_path}/internal/app/common/model/entity",'commonApi': f"{package_path}/api/v1/common",'do': f"{package_path}/internal/app/{package}/model/do",'SystemS': f"{package_path}/internal/app/system/service",'dao_internal': f"{package_path}/internal/app/{package}/dao/internal",'package': package,'packageFirstUpper': package.capitalize()}infos['package_info'] = package_info# 获取文件信息file_info = iu.get_file_info(table_name)file_info['package'] = packagefile_info['packageFirstUpper'] = package.capitalize()# 添加表信息table_info = {'name': table_name,'lowerName': table_name.lower(),'upperName': table_name.upper(),'firstUpperName': ''.join(word.capitalize() for word in table_name.split('_')),'camelName': ''.join(word.capitalize() for word in table_name.split('_'))[0].lower() + ''.join(word.capitalize() for word in table_name.split('_'))[1:],'comment': table_config.get('comment', ''),'columns': infos['columns'],'primaryKeys': infos['primary_keys'],'foreignKeys': infos['foreign_keys']}infos['table_info'] = table_info# 设置模板路径template_base_path = Path('template')template_path = template_base_path.joinpath(template_name)# 设置输出路径output_path = Path(output_path).resolve()if not output_path.exists():output_path.mkdir(parents=True)# 渲染模板templates = Environment(loader=FileSystemLoader(template_path), lstrip_blocks=True, trim_blocks=True)for template in templates.list_templates():tl = templates.get_template(template)item_template_path = Path(tl.filename)# 渲染相对路径relative_path = Template(str(item_template_path.parent.relative_to(template_path))).render(file_info)log.info(f"Processing template: {relative_path}")# 渲染文件名(保持原始扩展名)template_stem = item_template_path.stem # 获取不带.j2的文件名if '.' in template_stem: # 如果文件名中包含扩展名name_part, ext = template_stem.rsplit('.', 1) # 分离名称和扩展名file_name = Template(name_part).render(file_info) + '.' + extelse:file_name = Template(template_stem).render(file_info)render_str = tl.render(infos)parent_output = output_path.joinpath(relative_path)if not parent_output.exists():parent_output.mkdir(parents=True)file_path = parent_output.joinpath(file_name)log.info(f"Generating file: {file_path}")# 跳过已存在的router.go和logic.go文件if Path(file_path).exists() and (str(item_template_path.stem) in ["router.go", "logic.go"]):continuewith open(file_path, 'w', encoding='utf-8') as f:f.write(render_str)def main():# 读取配置config_path = Path('config/application.yml').resolve()config_util = ConfigUtil(config_path)config = config_util.read_config()# 获取生成器配置generator_config = config['code_generator']generate_type = generator_config['generate_type']# 处理每个表的配置for table_config in generator_config['tables']:table_name = table_config['name']table_generate_type = table_config.get('generate_type', generate_type)# 生成前端代码if table_generate_type in ['frontend', 'all']:frontend_config = table_config['frontend']config_map = {'template_name': frontend_config['template_name'],'table_name': table_name,'output_path': frontend_config['output_path'],'primary_name': frontend_config['primary_name']}generate_code(config_map, table_config)# 生成后端代码if table_generate_type in ['backend', 'all']:backend_config = table_config['backend']config_map = {'template_name': backend_config['template_name'],'table_name': table_name,'output_path': backend_config['output_path'],'package': backend_config['package'],'package_path': backend_config['package_path']}generate_code(config_map, table_config)if __name__ == "__main__":main()
5.2 后端模板
项目结构
gfast模板:
- api
- api\v1
- api\v1\{{package}}
- api\v1\{{package}}\{{name}}.go.j2
- internal
- internal\app
- internal\app\{{package}}
- internal\app\{{package}}\controller
- internal\app\{{package}}\controller\{{name}}.go.j2
- internal\app\{{package}}\dao
- internal\app\{{package}}\dao\internal
- internal\app\{{package}}\dao\internal\{{name}}.go.j2
- internal\app\{{package}}\dao\{{name}}.go.j2
- internal\app\{{package}}\logic
- internal\app\{{package}}\logic\logic.go.j2
- internal\app\{{package}}\logic\{{name}}
- internal\app\{{package}}\logic\{{name}}\{{name}}.go.j2
- internal\app\{{package}}\model
- internal\app\{{package}}\model\do
- internal\app\{{package}}\model\do\{{name}}.go.j2
- internal\app\{{package}}\model\entity
- internal\app\{{package}}\model\entity\{{name}}.go.j2
- internal\app\{{package}}\model\{{name}}.go.j2
- internal\app\{{package}}\router
- internal\app\{{package}}\router\router.go.j2
- internal\app\{{package}}\service
- internal\app\{{package}}\service\{{name}}.go.j2
代码内容
api\v1{{package}}{{name}}.go.j2
package templateimport ("github.com/gogf/gf/v2/frame/g"commonApi "{{package_info.commonApi}}"model "{{package_info.model}}")type {{table_info.firstUpperName}}AddReq struct {g.Meta `path:"/{{table_info.camelName}}/add" method:"post" tags:"{{table_info.comment}}" summary:"{{table_info.comment}}-新增"`{% for item in field_info %}{% if item.camelName not in ['id','createdBy','updatedBy','createdAt','updatedAt'] %}{{ item.firstUpperName }} {{ item.type }} `json:"{{ item.camelName }}" v:"required#{{ item.comment }}不能为空"`{% endif %}{% endfor %}
}type {{table_info.firstUpperName}}AddRes struct {g.Meta `mime:"application/json" example:"string"`
}type {{table_info.firstUpperName}}DelReq struct {g.Meta `path:"/{{table_info.camelName}}/del" method:"delete" tags:"{{table_info.comment}}" summary:"{{table_info.comment}}-删除"`Id uint `json:"id" v:"required#id不能为空"`
}type {{table_info.firstUpperName}}DelRes struct {g.Meta `mime:"application/json" example:"string"`
}type {{table_info.firstUpperName}}BatchDelReq struct {g.Meta `path:"/{{table_info.camelName}}/batchdel" method:"delete" tags:"{{table_info.comment}}" summary:"{{table_info.comment}}-批量删除"`Ids []uint `json:"id" v:"required#id不能为空"`
}type {{table_info.firstUpperName}}BatchDelRes struct {g.Meta `mime:"application/json" example:"string"`
}type {{table_info.firstUpperName}}EditReq struct {g.Meta `path:"/{{table_info.camelName}}/edit" method:"put" tags:"{{table_info.comment}}" summary:"{{table_info.comment}}-修改"`{% for item in field_info %}{% if item.camelName not in ['createdBy','updatedBy','createdAt','updatedAt'] %}{{ item.firstUpperName }} {{ item.type }} `json:"{{ item.camelName }}" v:"required#{{ item.comment }}不能为空"`{% endif %}{% endfor %}
}type {{table_info.firstUpperName}}EditRes struct {g.Meta `mime:"application/json" example:"string"`
}type {{table_info.firstUpperName}}ListReq struct {g.Meta `path:"/{{table_info.camelName}}/list" method:"get" tags:"{{table_info.comment}}" summary:"{{table_info.comment}}-列表"`commonApi.PageReq{% for item in field_info %}{% if item.camelName not in ['id','createdBy','updatedBy','createdAt','updatedAt'] %}{{ item.firstUpperName }} {{ item.type }} `json:"{{ item.camelName }}" v:"required#{{ item.comment }}不能为空"`{% endif %}{% endfor %}
}type {{table_info.firstUpperName}}ListRes struct {g.Meta `mime:"application/json" example:"string"`commonApi.ListRes{{table_info.firstUpperName}}List []*model.{{table_info.firstUpperName}}Info `json:"{{table_info.camelName}}List"`
}type {{table_info.firstUpperName}}DetailReq struct {g.Meta `path:"/{{table_info.camelName}}/detail" method:"get" tags:"{{table_info.comment}}" summary:"{{table_info.comment}}-详情"`Id uint `json:"id" v:"required#id不能为空"`
}type {{table_info.firstUpperName}}DetailRes struct {g.Meta `mime:"application/json" example:"string"`*model.{{table_info.firstUpperName}}Info
}
internal\app{{package}}\controller{{name}}.go.j2
package controllerimport ("context"api "{{package_info.api}}"service "{{package_info.service}}"consts "{{package_info.consts}}"
)var {{table_info.firstUpperName}} = {{table_info.camelName}}Controller{}type {{table_info.camelName}}Controller struct {BaseController
}func (c *{{table_info.camelName}}Controller) Add(ctx context.Context, req *api.{{table_info.firstUpperName}}AddReq) (res *api.{{table_info.firstUpperName}}AddRes, err error) {res = new(api.{{table_info.firstUpperName}}AddRes)err = service.{{table_info.firstUpperName}}().Add(ctx, req)return
}func (c *{{table_info.camelName}}Controller) List(ctx context.Context, req *api.{{table_info.firstUpperName}}ListReq) (res *api.{{table_info.firstUpperName}}ListRes, err error) {res = new(api.{{table_info.firstUpperName}}ListRes)if req.PageSize == 0 {req.PageSize = consts.PageSize}if req.PageNum == 0 {req.PageNum = 1}total, {{table_info.camelName}}s, err := service.{{table_info.firstUpperName}}().List(ctx, req)res.Total = totalres.CurrentPage = req.PageNumres.{{table_info.firstUpperName}}List = {{table_info.camelName}}sreturn
}func (c *{{table_info.camelName}}Controller) Get(ctx context.Context, req *api.{{table_info.firstUpperName}}DetailReq) (res *api.{{table_info.firstUpperName}}DetailRes, err error) {res = new(api.{{table_info.firstUpperName}}DetailRes)service.{{table_info.firstUpperName}}().GetById(ctx, req.Id)return
}func (c *{{table_info.camelName}}Controller) Edit(ctx context.Context, req *api.{{table_info.firstUpperName}}EditReq) (res *api.{{table_info.firstUpperName}}EditRes, err error) {err = service.{{table_info.firstUpperName}}().Edit(ctx, req)return
}func (c *{{table_info.camelName}}Controller) Delete(ctx context.Context, req *api.{{table_info.firstUpperName}}DelReq) (res *api.{{table_info.firstUpperName}}DelRes, err error) {err = service.{{table_info.firstUpperName}}().Delete(ctx, req.Id)return
}func (c *{{table_info.camelName}}Controller) BatchDelete(ctx context.Context, req *api.{{table_info.firstUpperName}}BatchDelReq) (res *api.{{table_info.firstUpperName}}BatchDelRes, err error) {err = service.{{table_info.firstUpperName}}().BatchDelete(ctx, req.Ids)return
}
internal\app{{package}}\dao\internal{{name}}.go.j2
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================package internalimport ("context""github.com/gogf/gf/v2/database/gdb""github.com/gogf/gf/v2/frame/g"
)// {{table_info.firstUpperName}}Dao is the data access object for table cts_voice.
type {{table_info.firstUpperName}}Dao struct {table string // table is the underlying table name of the DAO.group string // group is the database configuration group name of current DAO.columns {{table_info.firstUpperName}}Columns // columns contains all the column names of Table for convenient usage.
}// {{table_info.firstUpperName}}Columns defines and stores column names for table {{table_info.Name}}.
type {{table_info.firstUpperName}}Columns struct {{% for item in field_info %}{{ item.firstUpperName }} string //{{ item.comment }}{% endfor %}
}// {{table_info.camelName}}Columns holds the columns for table {{table_info.Name}}.
var {{table_info.camelName}}Columns = {{table_info.firstUpperName}}Columns{{% for item in field_info %}{{ item.firstUpperName }}: "{{ item.name }}",{% endfor %}
}// New{{table_info.firstUpperName}}Dao creates and returns a new DAO object for table data access.
func New{{table_info.firstUpperName}}Dao() *{{table_info.firstUpperName}}Dao {return &{{table_info.firstUpperName}}Dao{group: "default",table: "{{table_info.name}}",columns: {{table_info.camelName}}Columns,}
}// DB retrieves and returns the underlying raw database management object of current DAO.
func (dao *{{table_info.firstUpperName}}Dao) DB() gdb.DB {return g.DB(dao.group)
}// Table returns the table name of current dao.
func (dao *{{table_info.firstUpperName}}Dao) Table() string {return dao.table
}// Columns returns all column names of current dao.
func (dao *{{table_info.firstUpperName}}Dao) Columns() {{table_info.firstUpperName}}Columns {return dao.columns
}// Group returns the configuration group name of database of current dao.
func (dao *{{table_info.firstUpperName}}Dao) Group() string {return dao.group
}// Ctx creates and returns the Model for current DAO, It automatically sets the context for current operation.
func (dao *{{table_info.firstUpperName}}Dao) Ctx(ctx context.Context) *gdb.Model {return dao.DB().Model(dao.table).Safe().Ctx(ctx)
}// Transaction wraps the transaction logic using function f.
// It rollbacks the transaction and returns the error from function f if it returns non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note that, you should not Commit or Rollback the transaction in function f
// as it is automatically handled by this function.
func (dao *{{table_info.firstUpperName}}Dao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {return dao.Ctx(ctx).Transaction(ctx, f)
}
internal\app{{package}}\dao{{name}}.go.j2
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================package daoimport (internal "{{package_info.dao_internal}}"
)// internal{{table_info.firstUpperName}}Dao is internal type for wrapping internal DAO implements.
type internal{{table_info.firstUpperName}}Dao = *internal.{{table_info.firstUpperName}}Dao// {{table_info.camelName}}Dao is the data access object for table {{table_info.Name}}.
// You can define custom methods on it to extend its functionality as you wish.
type {{table_info.camelName}}Dao struct {internal{{table_info.firstUpperName}}Dao
}var (// {{table_info.firstUpperName}}Dao is globally public accessible object for table cts_voice operations.{{table_info.firstUpperName}} = {{table_info.camelName}}Dao{internal.New{{table_info.firstUpperName}}Dao(),}
)// Fill with you ideas below.
internal\app{{package}}\logic\logic.go.j2
package logicimport _ "github.com/tiger1103/gfast/v3/internal/app/{{package_info.package}}/logic/{{table_info.name}}"
internal\app{{package}}\logic{{name}}{{name}}.go.j2
package {{table_info.name}}import ("context""fmt""github.com/gogf/gf/v2/frame/g"api "{{package_info.api}}"dao "{{package_info.dao}}"model "{{package_info.model}}"do "{{package_info.do}}"service "{{package_info.service}}"SystemS "{{package_info.SystemS}}"liberr "{{package_info.liberr}}"
)func init() {service.Register{{table_info.firstUpperName}}(New())
}func New() *s{{table_info.firstUpperName}} {return &s{{table_info.firstUpperName}}{}
}type s{{table_info.firstUpperName}} struct {
}func (s s{{table_info.firstUpperName}}) List(ctx context.Context, req *api.{{table_info.firstUpperName}}ListReq) (total interface{}, {{table_info.camelName}}List []*model.{{table_info.firstUpperName}}Info, err error) {err = g.Try(ctx, func(ctx context.Context) {m := dao.{{table_info.firstUpperName}}.Ctx(ctx)columns := dao.{{table_info.firstUpperName}}.Columns()//TODO 根据实际情况修改{% for item in field_info %}{% if item.camelName not in ['id','createdBy','updatedBy','createdAt','updatedAt'] %}if req.{{ item.firstUpperName }} != "" {m = m.Where(columns.{{ item.firstUpperName }}+" = ?", req.{{ item.firstUpperName }})// like//m = m.Where(fmt.Sprintf("%s like ?", columns.{{ item.firstUpperName }}), "%"+req.{{ item.firstUpperName }}+"%")}{% endif %}{% endfor %}total, err = m.Count()liberr.ErrIsNil(ctx, err, "获取{{table_info.comment}}列表失败")orderBy := req.OrderByif orderBy == "" {orderBy = "created_at desc"}err = m.Page(req.PageNum, req.PageSize).Order(orderBy).Scan(&{{table_info.camelName}}List)liberr.ErrIsNil(ctx, err, "获取{{table_info.comment}}列表失败")})return
}func (s s{{table_info.firstUpperName}}) Add(ctx context.Context, req *api.{{table_info.firstUpperName}}AddReq) (err error) {err = g.Try(ctx, func(ctx context.Context) {// TODO 查询是否已经存在// add_, err = dao.{{table_info.firstUpperName}}.Ctx(ctx).Insert(do.{{table_info.firstUpperName}}{{% for item in field_info %}{% if item.camelName not in ['id','createdBy','updatedBy','createdAt','updatedAt'] %}{{ item.firstUpperName }}: req.{{ item.firstUpperName }}, // {{ item.comment }}{% endif %}{% endfor %}CreatedBy: SystemS.Context().GetUserId(ctx),UpdatedBy: SystemS.Context().GetUserId(ctx),})liberr.ErrIsNil(ctx, err, "新增{{table_info.comment}}失败")})return
}func (s s{{table_info.firstUpperName}}) Edit(ctx context.Context, req *api.{{table_info.firstUpperName}}EditReq) (err error) {err = g.Try(ctx, func(ctx context.Context) {_, err = s.GetById(ctx, req.Id)liberr.ErrIsNil(ctx, err, "获取{{table_info.comment}}失败")//TODO 根据名称等查询是否存在//编辑_, err = dao.{{table_info.firstUpperName}}.Ctx(ctx).WherePri(req.Id).Update(do.{{table_info.firstUpperName}}{{% for item in field_info %}{% if item.camelName not in ['createdBy','updatedBy','createdAt','updatedAt'] %}{{ item.firstUpperName }}: req.{{ item.firstUpperName }}, // {{ item.comment }}{% endif %}{% endfor %}})liberr.ErrIsNil(ctx, err, "修改{{table_info.comment}}失败")})return
}func (s s{{table_info.firstUpperName}}) Delete(ctx context.Context, id uint) (err error) {err = g.Try(ctx, func(ctx context.Context) {_, err = dao.{{table_info.firstUpperName}}.Ctx(ctx).WherePri(id).Delete()liberr.ErrIsNil(ctx, err, "删除{{table_info.comment}}失败")})return
}func (s s{{table_info.firstUpperName}}) BatchDelete(ctx context.Context, ids []uint) (err error) {err = g.Try(ctx, func(ctx context.Context) {_, err = dao.{{table_info.firstUpperName}}.Ctx(ctx).Where(dao.{{table_info.firstUpperName}}.Columns().Id+" in(?)", ids).Delete()liberr.ErrIsNil(ctx, err, "批量删除{{table_info.comment}}失败")})return
}func (s s{{table_info.firstUpperName}}) GetById(ctx context.Context, id uint) (res *model.{{table_info.firstUpperName}}Info, err error) {err = g.Try(ctx, func(ctx context.Context) {err = dao.{{table_info.firstUpperName}}.Ctx(ctx).Where(fmt.Sprintf("%s=?", dao.{{table_info.firstUpperName}}.Columns().Id), id).Scan(&res)liberr.ErrIsNil(ctx, err, "获取{{table_info.comment}}失败")})return
}
internal\app{{package}}\model\do{{name}}.go.j2
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================package doimport ("github.com/gogf/gf/v2/frame/g"
)// {{table_info.firstUpperName}} is the golang structure of table {{table_info.name}} for DAO operations like Where/Data.
type {{table_info.firstUpperName}} struct {g.Meta `orm:"table:{{table_info.name}}, do:true"`{% for item in field_info %}{{ item.firstUpperName }} interface{} //{{ item.comment }}{% endfor %}
}
internal\app{{package}}\model\entity{{name}}.go.j2
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================package entityimport "github.com/gogf/gf/v2/os/gtime"// {{table_info.firstUpperName}} is the golang structure for table {{table_info.name}}.
type {{table_info.firstUpperName}} struct {{% for item in field_info %}{{ item.firstUpperName }} {{ item.type }} `json:"{{ item.camelName }}" description:"{{ item.comment }}"`{% endfor %}
}
internal\app{{package}}\model{{name}}.go.j2
package modelimport "github.com/gogf/gf/v2/os/gtime"type {{table_info.firstUpperName}}Info struct {{% for item in field_info %}{{ item.firstUpperName }} {{ item.type }} `orm:"{{item.name}}" json:"{{item.camelName}}"` // {{ item.comment }}{% endfor %}
}
internal\app{{package}}\router\router.go.j2
/*
* @desc:后台路由
* @company:云南奇讯科技有限公司
* @Author: yixiaohu
* @Date: 2022/2/18 17:34*/package routerimport ("context""github.com/gogf/gf/v2/net/ghttp"_ "github.com/tiger1103/gfast/v3/internal/app/{{package_info.package}}/logic""github.com/tiger1103/gfast/v3/internal/app/{{package_info.package}}/controller""github.com/tiger1103/gfast/v3/internal/app/system/service""github.com/tiger1103/gfast/v3/library/libRouter"
)var R = new(Router)type Router struct{}func (router *Router) BindController(ctx context.Context, group *ghttp.RouterGroup) {group.Group("/{{package_info.package}}", func(group *ghttp.RouterGroup) {//登录验证拦截service.GfToken().Middleware(group)//context拦截器group.Middleware(service.Middleware().Ctx, service.Middleware().Auth)//后台操作日志记录group.Hook("/*", ghttp.HookAfterOutput, service.OperateLog().OperationLog)group.Bind(controller.{{table_info.firstUpperName}},)//自动绑定定义的控制器if err := libRouter.RouterAutoBind(ctx, router, group); err != nil {panic(err)}})
}
internal\app{{package}}\service{{name}}.go.j2
package serviceimport ("context"api "{{package_info.api}}"model "{{package_info.model}}"
)type I{{table_info.firstUpperName}} interface {List(ctx context.Context, req *api.{{table_info.firstUpperName}}ListReq) (total interface{}, res []*model.{{table_info.firstUpperName}}Info, err error)Add(ctx context.Context, req *api.{{table_info.firstUpperName}}AddReq) (err error)Edit(ctx context.Context, req *api.{{table_info.firstUpperName}}EditReq) (err error)Delete(ctx context.Context, id uint) (err error)BatchDelete(ctx context.Context, ids []uint) (err error)GetById(ctx context.Context, id uint) (res *model.{{table_info.firstUpperName}}Info, err error)
}var local{{table_info.firstUpperName}} I{{table_info.firstUpperName}}func {{table_info.firstUpperName}}() I{{table_info.firstUpperName}} {if local{{table_info.firstUpperName}} == nil {panic("implement not found for interface I{{table_info.firstUpperName}}, forgot register?")}return local{{table_info.firstUpperName}}
}func Register{{table_info.firstUpperName}}(i I{{table_info.firstUpperName}}) {local{{table_info.firstUpperName}} = i
}
5.3 前端模板
项目结构
gfast前端模板2:
- src
- src\api
- src\api\{{package}}
- src\api\{{package}}\{{camel_name}}
- src\api\{{package}}\{{camel_name}}\index.ts.j2
- src\views
- src\views\{{package}}
- src\views\{{package}}\{{camel_name}}
- src\views\{{package}}\{{camel_name}}\component
- src\views\{{package}}\{{camel_name}}\component\edit{{first_upper_name}}.vue.j2
- src\views\{{package}}\{{camel_name}}\index.vue.j2
代码内容
src\api{{package}}{{camel_name}}\index.ts.j2
import request from '/@/utils/request';export function get{{table_info.firstUpperName}}List(query?:Object) {return request({url: '/api/v1/{{package_info.package}}/{{table_info.camelName}}/list',method: 'get',params:query})
}export function add{{table_info.firstUpperName}}(data:object) {return request({url: '/api/v1/{{package_info.package}}/{{table_info.camelName}}/add',method: 'post',data:data})
}export function edit{{table_info.firstUpperName}}(data:object) {return request({url: '/api/v1/{{package_info.package}}/{{table_info.camelName}}/edit',method: 'put',data:data})
}export function delete{{table_info.firstUpperName}}(id:number) {return request({url: '/api/v1/{{package_info.package}}/{{table_info.camelName}}/del',method: 'delete',data:{id}})
}export function batchDelete{{table_info.firstUpperName}}(ids:number[]) {return request({url: '/api/v1/{{package_info.package}}/{{table_info.camelName}}/batchdel',method: 'delete',data:{ids}})
}
src\views{{package}}{{camel_name}}\component\edit{{first_upper_name}}.vue.j2
<template><div class="system-edit-post-container"><el-dialog v-model="state.isShowDialog" width="769px"><template #header><div>{{'{{'}}(state.formData.id===0?'添加':'修改')+'{{table_info.comment}}'{{'}}'}}</div></template><el-form ref="formRef" :model="state.formData" :rules="state.rules" size="default" label-width="90px">{% for item in field_info %}{% if item.camelName not in ['id', 'createdBy','updatedBy','createdAt','updatedAt'] %}<el-form-item label="{{ item.comment }}" prop="{{ item.camelName }}"><el-input v-model="state.formData.{{ item.camelName }}" placeholder="请输入{{ item.comment }}名称" clearable /></el-form-item>{% endif %}{% endfor %}</el-form><template #footer><span class="dialog-footer"><el-button @click="onCancel" size="default">取 消</el-button><el-button type="primary" @click="onSubmit" size="default" :loading="state.loading">{{'{{'}}state.formData.id===0?'新 增':'修 改'{{'}}'}}</el-button></span></template></el-dialog></div>
</template><script setup lang="ts">
import { ref, reactive } from 'vue';
import { add{{ table_info.firstUpperName }}, edit{{ table_info.firstUpperName }} } from "/@/api/{{ package_info.package }}/{{ table_info.camelName }}/index.ts";
import { ElMessage, ElForm } from 'element-plus';interface DialogRow {{% for item in field_info %}{% if item.camelName not in ['createdBy','updatedBy','createdAt','updatedAt'] %}{{ item.camelName }}: {{ item.tsType }}; // {{ item.comment }}{% endif %}{% endfor %}
}const formRef = ref<InstanceType<typeof ElForm> | null>(null);
const state = reactive({loading: false,isShowDialog: false,formData: {{% for item in field_info %}{% if item.camelName not in ['createdBy','updatedBy','createdAt','updatedAt'] %}{% if item.tsType == 'string' %}{{ item.camelName }}: '', // {{ item.comment }}{% else %}{{ item.camelName }}: 0, // {{ item.comment }}{% endif %}{% endif %}{% endfor %}} as DialogRow,rules: {{% for item in field_info %}{% if item.camelName not in ['id', 'createdBy','updatedBy','createdAt','updatedAt'] %}{{ item.camelName }}: [{ required: true, message: "{{ item.comment }}不能为空", trigger: "blur" }],{% endif %}{% endfor %}},
});const openDialog = (row?: DialogRow) => {resetForm();if (row) {state.formData = row;}state.isShowDialog = true;
};const closeDialog = () => {state.isShowDialog = false;
};const onCancel = () => {closeDialog();
};const onSubmit = () => {const formWrap = formRef.value;if (!formWrap) {return;}formWrap.validate((valid: boolean) => {if (!valid) {return;}state.loading = true;if (state.formData.id === 0) {add{{ table_info.firstUpperName }}(state.formData).then(() => {ElMessage.success('{{ table_info.comment }}添加成功');closeDialog();emit('get{{ table_info.firstUpperName }}List');}).finally(() => {state.loading = false;});} else {edit{{ table_info.firstUpperName }}(state.formData).then(() => {ElMessage.success('{{ table_info.comment }}编辑成功');closeDialog();emit('get{{ table_info.firstUpperName }}List');}).finally(() => {state.loading = false;});}});
};const resetForm = () => {state.formData = {{% for item in field_info %}{% if item.camelName not in ['createdBy','updatedBy','createdAt','updatedAt'] %}{% if item.tsType == 'string' %}{{ item.camelName }}: '', // {{ item.comment }}{% else %}{{ item.camelName }}: 0, // {{ item.comment }}{% endif %}{% endif %}{% endfor %}} as DialogRow;if (formRef.value) {formRef.value.resetFields();}
};defineExpose({openDialog,closeDialog,
});const emit = defineEmits(['get{{ table_info.firstUpperName }}List']);
</script><style scoped lang="scss">
</style>
src\views{{package}}{{camel_name}}\index.vue.j2
<template><div class="system-{{table_info.camelName}}-container"><el-card shadow="hover"><div class="system-{{table_info.camelName}}-search mb15"><el-form :inline="true">{% for item in front_info.serach_columns %}<el-form-item label="{{item.comment}}"><el-input size="default" v-model="state.tableData.param.{{item.camelName}}" style="width: 240px" placeholder="请输入{{item.comment}}" class="w-50 m-2" clearable/></el-form-item>{% endfor %}<el-form-item><el-button size="default" type="primary" class="ml10" @click="{{table_info.camelName}}List"><el-icon><ele-Search /></el-icon>查询</el-button><el-button size="default" type="success" class="ml10" @click="onOpenAdd{{table_info.firstUpperName}}"><el-icon><ele-FolderAdd /></el-icon>新增{{table_info.comment}}</el-button><el-button size="default" type="danger" class="ml10" @click="onRowDel(null)"><el-icon><ele-Delete /></el-icon>删除{{table_info.comment}}</el-button></el-form-item></el-form></div><el-table :data="state.tableData.data" style="width: 100%" @selection-change="handleSelectionChange"><el-table-column type="selection" width="55" align="center" /><el-table-column type="index" label="序号" width="60" />{% for item in field_info %}{% if item.camelName not in ['id', 'createdBy','updatedBy','createdAt','updatedAt'] %}<el-table-column prop="{{item.camelName}}" label="{{item.comment}}" show-overflow-tooltip></el-table-column>{% endif %}{% endfor %}<el-table-column label="操作" width="200"><template #default="scope"><el-button size="small" text type="primary" @click="onOpenEdit{{table_info.firstUpperName}}(scope.row)">修改</el-button><el-button size="small" text type="primary" @click="onRowDel(scope.row)">删除</el-button></template></el-table-column></el-table><paginationv-show="state.tableData.total>0":total="state.tableData.total"v-model:page="state.tableData.param.pageNum"v-model:limit="state.tableData.param.pageSize"@pagination="{{table_info.camelName}}List"/></el-card><Edit{{table_info.firstUpperName}} ref="edit{{table_info.firstUpperName}}Ref" @get{{table_info.firstUpperName}}List="{{table_info.camelName}}List"/></div>
</template><script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus';
import Edit{{table_info.firstUpperName}} from '/@/views/{{package_info.package}}/{{table_info.camelName}}/component/edit{{table_info.firstUpperName}}.vue';
import { batchDelete{{table_info.firstUpperName}}, get{{table_info.firstUpperName}}List } from "/@/api/{{package_info.package}}/{{table_info.camelName}}";// 定义接口来定义对象的类型
interface TableData {{% for item in field_info %}{{ item.camelName }}: {{ item.tsType }}; // {{ item.comment }}{% endfor %}
}interface TableDataState {ids: number[];tableData: {data: Array<TableData>;total: number;loading: boolean;param: {{% for item in front_info.serach_columns %}{{ item.camelName }}: {{ item.type }}; // {{ item.comment }}{% endfor %}pageNum: number; // 当前页码pageSize: number; // 每页条数};};
}const edit{{table_info.firstUpperName}}Ref = ref();
const state = reactive<TableDataState>({ids: [],tableData: {data: [],total: 0,loading: false,param: {{% for item in front_info.serach_columns %}{% if item.type == 'string' %}{{ item.camelName }}: '',{% else %}{{ item.camelName }}: 0,{% endif %}{% endfor %}pageNum: 1,pageSize: 10,},},
});// 初始化表格数据
const initTableData = () => {{{ table_info.camelName }}List();
};// 查询{{ table_info.comment }}列表数据
const {{ table_info.camelName }}List = () => {get{{ table_info.firstUpperName }}List(state.tableData.param).then(res => {state.tableData.data = res.data.{{ table_info.camelName }}List ?? [];state.tableData.total = res.data.total;});
};// 打开新增{{ table_info.comment }}弹窗
const onOpenAdd{{ table_info.firstUpperName }} = () => {edit{{ table_info.firstUpperName }}Ref.value.openDialog();
};// 打开修改{{ table_info.comment }}弹窗
const onOpenEdit{{ table_info.firstUpperName }} = (row: Object) => {row = Object.assign({}, row);edit{{ table_info.firstUpperName }}Ref.value.openDialog(row);
};// 删除{{ table_info.comment }}
const onRowDel = (row: any) => {let msg = '你确定要删除所选{{ table_info.comment }}?';let ids: number[] = [];if (row) {msg = `此操作将永久删除{{ table_info.comment }}:“${row.{{ front_info.primarkName }}}”,是否继续?`;ids = [row.id];} else {ids = state.ids;}if (ids.length === 0) {ElMessage.error('请选择要删除的数据。');return;}ElMessageBox.confirm(msg, '提示', {confirmButtonText: '确认',cancelButtonText: '取消',type: 'warning',}).then(() => {batchDelete{{ table_info.firstUpperName }}(ids).then(() => {ElMessage.success('删除成功');{{ table_info.camelName }}List();});}).catch(() => {});
};// 分页改变
const onHandleSizeChange = (val: number) => {state.tableData.param.pageSize = val;{{ table_info.camelName }}List();
};// 分页改变
const onHandleCurrentChange = (val: number) => {state.tableData.param.pageNum = val;{{ table_info.camelName }}List();
};// 多选框选中数据
const handleSelectionChange = (selection: Array<TableData>) => {state.ids = selection.map(item => item.id);
};// 页面加载时
onMounted(() => {initTableData();
});
</script>