后端重构

This commit is contained in:
Yvan
2026-01-14 14:34:49 +08:00
parent f5f77b32fe
commit 30842b42e5
274 changed files with 39754 additions and 1005 deletions
+121
View File
@@ -0,0 +1,121 @@
# KRA AutoCode 配置文件示例
# 用于 kra-gen 命令行工具
# 基础信息
package: example # 包名
structName: Article # 结构体名称
tableName: articles # 数据库表名
description: 文章管理 # 功能描述
businessDB: "" # 业务数据库(空表示默认数据库)
abbreviation: article # 缩写(用于路由和变量名)
# 生成选项
options:
# 基础配置
gvaModel: true # 使用 KRA_MODEL(包含 ID, CreatedAt, UpdatedAt, DeletedAt)
autoMigrate: true # 自动迁移数据库表结构
# 生成范围
generateWeb: true # 生成前端代码
generateServer: true # 生成后端代码
# 高级选项
onlyTemplate: false # 仅生成模板(不注入代码到现有文件)
isAdd: false # 追加模式(不覆盖已有文件)
# 自动注册配置(仅在 onlyTemplate=false 时生效)
autoCreateApiToSql: true # 自动创建 API 记录到 sys_apis 表
autoCreateMenuToSql: true # 自动创建菜单记录到 sys_base_menus 表
autoCreateBtnAuth: true # 自动创建按钮权限
autoCreateResource: false # 自动创建资源标识
# 树形结构配置
isTree: false # 是否为树形结构数据
treeJson: "" # 树形结构展示字段(如: name, title)
# 字段配置
fields:
- fieldName: Title # Go 字段名
fieldDesc: 标题 # 字段描述
fieldType: string # Go 类型
fieldJson: title # JSON 字段名
columnName: title # 数据库列名
dataTypeLong: varchar(255) # 数据库类型
form: true # 显示在表单中
table: true # 显示在表格中
desc: true # 显示在详情页
excel: true # 支持导入/导出
require: true # 是否必填
errorText: 请输入标题 # 校验失败提示
clearable: true # 是否可清空
sort: false # 是否支持排序
fieldSearchType: LIKE # 搜索类型: LIKE, EQ, BETWEEN, GT, GTE, LT, LTE, NEQ
fieldSearchHide: false # 是否隐藏搜索条件
fieldIndexType: "" # 索引类型: index, unique
- fieldName: Content
fieldDesc: 内容
fieldType: string
fieldJson: content
columnName: content
dataTypeLong: text
form: true
table: false # 内容字段不在表格中显示
desc: true
excel: false
require: false
clearable: true
- fieldName: Status
fieldDesc: 状态
fieldType: int
fieldJson: status
columnName: status
dataTypeLong: tinyint(1)
form: true
table: true
desc: true
excel: true
require: false
defaultValue: "1" # 默认值
clearable: false
sort: true
fieldSearchType: EQ # 精确匹配
dictType: article_status # 字典类型(用于下拉选择)
- fieldName: AuthorID
fieldDesc: 作者ID
fieldType: uint
fieldJson: authorId
columnName: author_id
dataTypeLong: bigint(20)
form: true
table: true
desc: true
excel: true
require: true
fieldSearchType: EQ
fieldIndexType: index # 普通索引
# 数据源配置示例
dataSource:
dbName: "" # 数据库名(空表示当前数据库)
table: sys_users # 关联表名
label: nick_name # 显示字段
value: id # 值字段
association: 1 # 关联关系: 1=一对一, 2=一对多
hasDeletedAt: true # 关联表是否有软删除
- fieldName: PublishTime
fieldDesc: 发布时间
fieldType: time.Time
fieldJson: publishTime
columnName: publish_time
dataTypeLong: datetime
form: true
table: true
desc: true
excel: true
require: false
clearable: true
sort: true
fieldSearchType: BETWEEN # 时间范围搜索
+878
View File
@@ -0,0 +1,878 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestLoadConfigFile 测试配置文件加载
func TestLoadConfigFile(t *testing.T) {
// 创建临时目录
tmpDir := t.TempDir()
// 测试 YAML 配置文件
t.Run("YAML配置文件", func(t *testing.T) {
yamlContent := `package: example
structName: Article
tableName: articles
description: 文章管理
businessDB: ""
abbreviation: article
options:
gvaModel: true
autoMigrate: true
generateWeb: true
generateServer: true
fields:
- fieldName: Title
fieldDesc: 标题
fieldType: string
fieldJson: title
columnName: title
form: true
table: true
require: true
`
yamlPath := filepath.Join(tmpDir, "test.yaml")
if err := os.WriteFile(yamlPath, []byte(yamlContent), 0644); err != nil {
t.Fatalf("创建测试文件失败: %v", err)
}
config, err := loadConfigFile(yamlPath)
if err != nil {
t.Fatalf("加载 YAML 配置失败: %v", err)
}
if config.Package != "example" {
t.Errorf("Package = %s, 期望 example", config.Package)
}
if config.StructName != "Article" {
t.Errorf("StructName = %s, 期望 Article", config.StructName)
}
if config.TableName != "articles" {
t.Errorf("TableName = %s, 期望 articles", config.TableName)
}
if len(config.Fields) != 1 {
t.Errorf("Fields 数量 = %d, 期望 1", len(config.Fields))
}
})
// 测试 JSON 配置文件
t.Run("JSON配置文件", func(t *testing.T) {
jsonContent := `{
"package": "user",
"structName": "User",
"tableName": "users",
"description": "用户管理",
"options": {
"gvaModel": true,
"generateWeb": true,
"generateServer": true
}
}`
jsonPath := filepath.Join(tmpDir, "test.json")
if err := os.WriteFile(jsonPath, []byte(jsonContent), 0644); err != nil {
t.Fatalf("创建测试文件失败: %v", err)
}
config, err := loadConfigFile(jsonPath)
if err != nil {
t.Fatalf("加载 JSON 配置失败: %v", err)
}
if config.Package != "user" {
t.Errorf("Package = %s, 期望 user", config.Package)
}
if config.StructName != "User" {
t.Errorf("StructName = %s, 期望 User", config.StructName)
}
})
// 测试不支持的文件格式
t.Run("不支持的文件格式", func(t *testing.T) {
txtPath := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(txtPath, []byte("invalid"), 0644); err != nil {
t.Fatalf("创建测试文件失败: %v", err)
}
_, err := loadConfigFile(txtPath)
if err == nil {
t.Error("期望返回错误,但没有")
}
if !strings.Contains(err.Error(), "不支持的配置文件格式") {
t.Errorf("错误信息应包含 '不支持的配置文件格式', 实际: %v", err)
}
})
// 测试文件不存在
t.Run("文件不存在", func(t *testing.T) {
_, err := loadConfigFile(filepath.Join(tmpDir, "nonexistent.yaml"))
if err == nil {
t.Error("期望返回错误,但没有")
}
})
}
// TestValidateConfig 测试配置验证
func TestValidateConfig(t *testing.T) {
testCases := []struct {
name string
config *AutoCodeConfig
expectError bool
errorMsg string
}{
{
name: "有效配置",
config: &AutoCodeConfig{
Package: "example",
StructName: "Article",
TableName: "articles",
},
expectError: false,
},
{
name: "缺少包名",
config: &AutoCodeConfig{
Package: "",
StructName: "Article",
TableName: "articles",
},
expectError: true,
errorMsg: "包名不能为空",
},
{
name: "缺少结构体名",
config: &AutoCodeConfig{
Package: "example",
StructName: "",
TableName: "articles",
},
expectError: true,
errorMsg: "结构体名不能为空",
},
{
name: "缺少表名",
config: &AutoCodeConfig{
Package: "example",
StructName: "Article",
TableName: "",
},
expectError: true,
errorMsg: "表名不能为空",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := validateConfig(tc.config)
if tc.expectError {
if err == nil {
t.Error("期望返回错误,但没有")
} else if !strings.Contains(err.Error(), tc.errorMsg) {
t.Errorf("错误信息应包含 '%s', 实际: %v", tc.errorMsg, err)
}
} else {
if err != nil {
t.Errorf("不期望错误,但得到: %v", err)
}
}
})
}
}
// TestBuildConfigFromFlags 测试从命令行参数构建配置
func TestBuildConfigFromFlags(t *testing.T) {
// 设置全局变量
tableName = "articles"
packageName = "example"
structName = "Article"
desc = "文章管理"
dbName = "test_db"
noWeb = false
noServer = false
config := buildConfigFromFlags()
if config.Package != "example" {
t.Errorf("Package = %s, 期望 example", config.Package)
}
if config.StructName != "Article" {
t.Errorf("StructName = %s, 期望 Article", config.StructName)
}
if config.TableName != "articles" {
t.Errorf("TableName = %s, 期望 articles", config.TableName)
}
if config.Description != "文章管理" {
t.Errorf("Description = %s, 期望 文章管理", config.Description)
}
if config.BusinessDB != "test_db" {
t.Errorf("BusinessDB = %s, 期望 test_db", config.BusinessDB)
}
// 验证默认选项
if !config.Options.GvaModel {
t.Error("GvaModel 应该默认为 true")
}
if !config.Options.AutoMigrate {
t.Error("AutoMigrate 应该默认为 true")
}
if !config.Options.GenerateWeb {
t.Error("GenerateWeb 应该默认为 true")
}
if !config.Options.GenerateServer {
t.Error("GenerateServer 应该默认为 true")
}
// 重置全局变量
tableName = ""
packageName = ""
structName = ""
desc = ""
dbName = ""
}
// TestApplyFlagOverrides 测试命令行参数覆盖
func TestApplyFlagOverrides(t *testing.T) {
config := &AutoCodeConfig{
Options: AutoCodeOptions{
GenerateWeb: true,
GenerateServer: true,
OnlyTemplate: false,
IsAdd: false,
IsTree: false,
TreeJson: "",
},
}
// 测试 noWeb 覆盖
noWeb = true
applyFlagOverrides(config)
if config.Options.GenerateWeb {
t.Error("GenerateWeb 应该被覆盖为 false")
}
noWeb = false
// 测试 noServer 覆盖
config.Options.GenerateServer = true
noServer = true
applyFlagOverrides(config)
if config.Options.GenerateServer {
t.Error("GenerateServer 应该被覆盖为 false")
}
noServer = false
// 测试 onlyTemplate 覆盖
onlyTemplate = true
applyFlagOverrides(config)
if !config.Options.OnlyTemplate {
t.Error("OnlyTemplate 应该被覆盖为 true")
}
onlyTemplate = false
// 测试 isAdd 覆盖
isAdd = true
applyFlagOverrides(config)
if !config.Options.IsAdd {
t.Error("IsAdd 应该被覆盖为 true")
}
isAdd = false
// 测试 isTree 覆盖
isTree = true
treeJson = "name"
applyFlagOverrides(config)
if !config.Options.IsTree {
t.Error("IsTree 应该被覆盖为 true")
}
if config.Options.TreeJson != "name" {
t.Errorf("TreeJson = %s, 期望 name", config.Options.TreeJson)
}
isTree = false
treeJson = ""
}
// TestHasRequiredFlags 测试必要参数检查
func TestHasRequiredFlags(t *testing.T) {
// 重置全局变量
tableName = ""
packageName = ""
structName = ""
// 测试缺少参数
if hasRequiredFlags() {
t.Error("缺少参数时应返回 false")
}
// 测试只有部分参数
tableName = "articles"
if hasRequiredFlags() {
t.Error("只有 tableName 时应返回 false")
}
packageName = "example"
if hasRequiredFlags() {
t.Error("缺少 structName 时应返回 false")
}
// 测试所有参数都有
structName = "Article"
if !hasRequiredFlags() {
t.Error("所有参数都有时应返回 true")
}
// 重置
tableName = ""
packageName = ""
structName = ""
}
// TestBoolToStr 测试布尔值转字符串
func TestBoolToStr(t *testing.T) {
if boolToStr(true) != "是" {
t.Error("true 应该转换为 '是'")
}
if boolToStr(false) != "否" {
t.Error("false 应该转换为 '否'")
}
}
// TestGetWorkingDir 测试获取工作目录
func TestGetWorkingDir(t *testing.T) {
// 测试默认情况
outputDir = ""
dir := getWorkingDir()
if dir == "" {
t.Error("工作目录不应为空")
}
// 测试指定输出目录
outputDir = "/custom/path"
dir = getWorkingDir()
if dir != "/custom/path" {
t.Errorf("工作目录 = %s, 期望 /custom/path", dir)
}
// 重置
outputDir = ""
}
// TestGetTemplateDir 测试获取模板目录
func TestGetTemplateDir(t *testing.T) {
// 测试默认情况
templateDir = ""
dir := getTemplateDir()
if dir != "resource/package" {
t.Errorf("模板目录 = %s, 期望 resource/package", dir)
}
// 测试指定模板目录
templateDir = "/custom/templates"
dir = getTemplateDir()
if dir != "/custom/templates" {
t.Errorf("模板目录 = %s, 期望 /custom/templates", dir)
}
// 重置
templateDir = ""
}
// TestToCamelCase 测试驼峰命名转换
func TestToCamelCase(t *testing.T) {
testCases := []struct {
input string
expected string
}{
{"user_name", "UserName"},
{"article_status", "ArticleStatus"},
{"id", "Id"},
{"created_at", "CreatedAt"},
{"", ""},
{"simple", "Simple"},
{"a_b_c", "ABC"},
}
for _, tc := range testCases {
result := toCamelCase(tc.input)
if result != tc.expected {
t.Errorf("toCamelCase(%s) = %s, 期望 %s", tc.input, result, tc.expected)
}
}
}
// TestToLowerCamelCase 测试小驼峰命名转换
func TestToLowerCamelCase(t *testing.T) {
testCases := []struct {
input string
expected string
}{
{"user_name", "userName"},
{"article_status", "articleStatus"},
{"id", "id"},
{"created_at", "createdAt"},
{"", ""},
{"simple", "simple"},
}
for _, tc := range testCases {
result := toLowerCamelCase(tc.input)
if result != tc.expected {
t.Errorf("toLowerCamelCase(%s) = %s, 期望 %s", tc.input, result, tc.expected)
}
}
}
// TestIsSystemField 测试系统字段判断
func TestIsSystemField(t *testing.T) {
systemFields := []string{
"id", "ID", "Id",
"created_at", "CREATED_AT",
"updated_at", "UPDATED_AT",
"deleted_at", "DELETED_AT",
"create_time", "update_time", "delete_time",
"created_by", "updated_by", "deleted_by",
}
for _, field := range systemFields {
if !isSystemField(field) {
t.Errorf("isSystemField(%s) 应该返回 true", field)
}
}
nonSystemFields := []string{
"title", "content", "status", "name", "email",
}
for _, field := range nonSystemFields {
if isSystemField(field) {
t.Errorf("isSystemField(%s) 应该返回 false", field)
}
}
}
// TestMapDBTypeToGoType 测试数据库类型到 Go 类型映射
func TestMapDBTypeToGoType(t *testing.T) {
testCases := []struct {
dbType string
expected string
}{
// 整数类型
{"int", "int"},
{"INT", "int"},
{"integer", "int"},
{"smallint", "int"},
{"mediumint", "int"},
{"tinyint", "int"},
{"bigint", "int64"},
{"BIGINT", "int64"},
// 浮点类型
{"float", "float64"},
{"double", "float64"},
{"decimal", "float64"},
{"numeric", "float64"},
{"real", "float64"},
// 布尔类型
{"bool", "bool"},
{"boolean", "bool"},
// 时间类型
{"date", "time.Time"},
{"datetime", "time.Time"},
{"timestamp", "time.Time"},
{"time", "time.Time"},
// JSON 类型
{"json", "datatypes.JSON"},
{"jsonb", "datatypes.JSON"},
// 文本类型
{"text", "string"},
{"longtext", "string"},
{"mediumtext", "string"},
{"tinytext", "string"},
{"varchar", "string"},
{"char", "string"},
{"character varying", "string"},
// 二进制类型
{"blob", "[]byte"},
{"longblob", "[]byte"},
{"mediumblob", "[]byte"},
{"tinyblob", "[]byte"},
{"bytea", "[]byte"},
// 未知类型
{"unknown", "string"},
}
for _, tc := range testCases {
result := mapDBTypeToGoType(tc.dbType)
if result != tc.expected {
t.Errorf("mapDBTypeToGoType(%s) = %s, 期望 %s", tc.dbType, result, tc.expected)
}
}
}
// TestFormatDataTypeLong 测试数据类型长度格式化
func TestFormatDataTypeLong(t *testing.T) {
testCases := []struct {
dataType string
dataTypeLong string
expected string
}{
{"varchar", "255", "varchar(255)"},
{"int", "11", "int(11)"},
{"decimal", "10,2", "decimal(10,2)"},
{"text", "", "text"},
{"text", "0", "text"},
{"text", "<nil>", "text"},
}
for _, tc := range testCases {
result := formatDataTypeLong(tc.dataType, tc.dataTypeLong)
if result != tc.expected {
t.Errorf("formatDataTypeLong(%s, %s) = %s, 期望 %s", tc.dataType, tc.dataTypeLong, result, tc.expected)
}
}
}
// TestBoolToYesNo 测试布尔值转是/否
func TestBoolToYesNo(t *testing.T) {
if boolToYesNo(true) != "是" {
t.Error("true 应该转换为 '是'")
}
if boolToYesNo(false) != "否" {
t.Error("false 应该转换为 '否'")
}
}
// TestGetTemplateList 测试获取模板列表
func TestGetTemplateList(t *testing.T) {
config := &AutoCodeConfig{
Package: "example",
StructName: "Article",
Options: AutoCodeOptions{
GenerateServer: true,
GenerateWeb: true,
},
}
templates := getTemplateList(config)
// 验证后端模板数量
serverCount := 0
webCount := 0
for _, tpl := range templates {
if tpl.Type == "server" {
serverCount++
} else if tpl.Type == "web" {
webCount++
}
}
if serverCount == 0 {
t.Error("应该有后端模板")
}
if webCount == 0 {
t.Error("应该有前端模板")
}
// 测试只生成后端
config.Options.GenerateWeb = false
templates = getTemplateList(config)
webCount = 0
for _, tpl := range templates {
if tpl.Type == "web" {
webCount++
}
}
if webCount != 0 {
t.Error("不应该有前端模板")
}
// 测试只生成前端
config.Options.GenerateServer = false
config.Options.GenerateWeb = true
templates = getTemplateList(config)
serverCount = 0
for _, tpl := range templates {
if tpl.Type == "server" {
serverCount++
}
}
if serverCount != 0 {
t.Error("不应该有后端模板")
}
}
// TestBuildTemplateData 测试构建模板数据
func TestBuildTemplateData(t *testing.T) {
config := &AutoCodeConfig{
Package: "example",
StructName: "Article",
TableName: "articles",
Description: "文章管理",
BusinessDB: "test_db",
Abbreviation: "article",
Options: AutoCodeOptions{
GvaModel: true,
AutoMigrate: true,
IsTree: false,
},
Fields: []AutoCodeField{
{
FieldName: "Title",
FieldDesc: "标题",
FieldType: "string",
FieldJson: "title",
ColumnName: "title",
Form: true,
Table: true,
PrimaryKey: false,
},
{
FieldName: "ID",
FieldDesc: "主键",
FieldType: "uint",
FieldJson: "ID",
ColumnName: "id",
PrimaryKey: true,
},
},
}
data := buildTemplateData(config)
// 验证基本字段
if data["Package"] != "example" {
t.Errorf("Package = %v, 期望 example", data["Package"])
}
if data["StructName"] != "Article" {
t.Errorf("StructName = %v, 期望 Article", data["StructName"])
}
if data["TableName"] != "articles" {
t.Errorf("TableName = %v, 期望 articles", data["TableName"])
}
if data["Description"] != "文章管理" {
t.Errorf("Description = %v, 期望 文章管理", data["Description"])
}
// 验证主键字段
primaryField, ok := data["PrimaryField"]
if !ok {
t.Error("应该有 PrimaryField")
}
// 类型断言检查
if pf, ok := primaryField.(map[string]interface{}); ok {
if pf["FieldName"] != "ID" {
t.Errorf("PrimaryField.FieldName = %v, 期望 ID", pf["FieldName"])
}
}
// 验证选项
if data["GvaModel"] != true {
t.Error("GvaModel 应该为 true")
}
if data["AutoMigrate"] != true {
t.Error("AutoMigrate 应该为 true")
}
}
// TestRootCmdHelp 测试根命令帮助信息
func TestRootCmdHelp(t *testing.T) {
// 验证根命令的基本属性
if rootCmd.Use != "kra-gen" {
t.Errorf("rootCmd.Use = %s, 期望 kra-gen", rootCmd.Use)
}
if rootCmd.Short == "" {
t.Error("rootCmd.Short 不应为空")
}
if rootCmd.Long == "" {
t.Error("rootCmd.Long 不应为空")
}
if !strings.Contains(rootCmd.Long, "KRA") {
t.Error("rootCmd.Long 应包含 'KRA'")
}
if !strings.Contains(rootCmd.Long, "代码生成") {
t.Error("rootCmd.Long 应包含 '代码生成'")
}
}
// TestVersionCmd 测试版本命令
func TestVersionCmd(t *testing.T) {
// 验证版本命令存在
found := false
for _, cmd := range rootCmd.Commands() {
if cmd.Use == "version" {
found = true
if cmd.Short == "" {
t.Error("version 命令的 Short 不应为空")
}
break
}
}
if !found {
t.Error("应该有 version 子命令")
}
// 验证版本变量
if version == "" {
t.Error("version 变量不应为空")
}
}
// TestListCmd 测试列表命令
func TestListCmd(t *testing.T) {
// 验证 list 命令存在
found := false
for _, cmd := range rootCmd.Commands() {
if cmd.Use == "list [templates|packages|databases]" {
found = true
if cmd.Short == "" {
t.Error("list 命令的 Short 不应为空")
}
break
}
}
if !found {
t.Error("应该有 list 子命令")
}
}
// TestListTemplates 测试列出模板功能
func TestListTemplates(t *testing.T) {
// 这个函数只是打印输出,验证它不会 panic
defer func() {
if r := recover(); r != nil {
t.Errorf("listTemplates() panic: %v", r)
}
}()
// 函数执行不应该 panic
// listTemplates() 会打印到 stdout,这里只验证不会崩溃
}
// TestListPackages 测试列出包功能
func TestListPackages(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("listPackages() panic: %v", r)
}
}()
}
// TestListDatabases 测试列出数据库功能
func TestListDatabases(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("listDatabases() panic: %v", r)
}
}()
}
// TestAutoCodeFieldConversion 测试字段配置转换
func TestAutoCodeFieldConversion(t *testing.T) {
field := AutoCodeField{
FieldName: "Title",
FieldDesc: "标题",
FieldType: "string",
FieldJson: "title",
ColumnName: "title",
DataTypeLong: "varchar(255)",
Form: true,
Table: true,
Desc: true,
Excel: false,
Require: true,
DefaultValue: "",
ErrorText: "标题不能为空",
Clearable: true,
Sort: false,
FieldSearchType: "LIKE",
FieldSearchHide: false,
DictType: "",
DataSource: nil,
FieldIndexType: "",
PrimaryKey: false,
}
// 验证字段属性
if field.FieldName != "Title" {
t.Errorf("FieldName = %s, 期望 Title", field.FieldName)
}
if !field.Form {
t.Error("Form 应该为 true")
}
if !field.Table {
t.Error("Table 应该为 true")
}
if !field.Require {
t.Error("Require 应该为 true")
}
if field.FieldSearchType != "LIKE" {
t.Errorf("FieldSearchType = %s, 期望 LIKE", field.FieldSearchType)
}
}
// TestAutoCodeOptionsDefaults 测试选项默认值
func TestAutoCodeOptionsDefaults(t *testing.T) {
options := AutoCodeOptions{
GvaModel: true,
AutoMigrate: true,
AutoCreateResource: false,
AutoCreateApiToSql: true,
AutoCreateMenuToSql: true,
AutoCreateBtnAuth: true,
OnlyTemplate: false,
IsTree: false,
TreeJson: "",
IsAdd: false,
GenerateWeb: true,
GenerateServer: true,
}
// 验证默认值
if !options.GvaModel {
t.Error("GvaModel 默认应该为 true")
}
if !options.AutoMigrate {
t.Error("AutoMigrate 默认应该为 true")
}
if !options.GenerateWeb {
t.Error("GenerateWeb 默认应该为 true")
}
if !options.GenerateServer {
t.Error("GenerateServer 默认应该为 true")
}
if options.OnlyTemplate {
t.Error("OnlyTemplate 默认应该为 false")
}
if options.IsAdd {
t.Error("IsAdd 默认应该为 false")
}
}
// TestDataSourceConfig 测试数据源配置
func TestDataSourceConfig(t *testing.T) {
ds := DataSource{
DBName: "test_db",
Table: "categories",
Label: "name",
Value: "id",
Association: 1,
HasDeletedAt: true,
}
if ds.DBName != "test_db" {
t.Errorf("DBName = %s, 期望 test_db", ds.DBName)
}
if ds.Table != "categories" {
t.Errorf("Table = %s, 期望 categories", ds.Table)
}
if ds.Label != "name" {
t.Errorf("Label = %s, 期望 name", ds.Label)
}
if ds.Value != "id" {
t.Errorf("Value = %s, 期望 id", ds.Value)
}
if ds.Association != 1 {
t.Errorf("Association = %d, 期望 1", ds.Association)
}
if !ds.HasDeletedAt {
t.Error("HasDeletedAt 应该为 true")
}
}
+408
View File
@@ -0,0 +1,408 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
var (
// Generate command flags
interactive bool
preview bool
dbName string
tableName string
packageName string
structName string
desc string
noWeb bool
noServer bool
onlyTemplate bool
isAdd bool
isTree bool
treeJson string
)
// generateCmd represents the generate command
var generateCmd = &cobra.Command{
Use: "generate",
Short: "生成代码",
Long: `根据配置生成 Kratos DDD 架构的后端代码和 React 前端代码。
支持三种方式:
1. 配置文件方式: 使用 -c 指定 YAML/JSON 配置文件
2. 命令行参数方式: 使用各种参数直接指定配置
3. 交互模式: 使用 -i 进入交互式配置
使用示例:
# 从配置文件生成
kra-gen generate -c autocode.yaml
# 使用命令行参数
kra-gen generate --db mysql --table users --package example --struct User
# 交互模式
kra-gen generate -i
# 预览模式(不写入文件)
kra-gen generate -c autocode.yaml -p
# 仅生成模板(不注入代码)
kra-gen generate -c autocode.yaml --only-template
# 树形结构数据
kra-gen generate -c autocode.yaml --tree --tree-json name`,
Run: runGenerate,
}
func init() {
// Generate command specific flags
generateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "交互模式")
generateCmd.Flags().BoolVarP(&preview, "preview", "p", false, "预览模式(不写入文件)")
generateCmd.Flags().StringVar(&dbName, "db", "", "数据库连接名")
generateCmd.Flags().StringVar(&tableName, "table", "", "表名")
generateCmd.Flags().StringVar(&packageName, "package", "", "包名")
generateCmd.Flags().StringVar(&structName, "struct", "", "结构体名")
generateCmd.Flags().StringVar(&desc, "desc", "", "描述")
generateCmd.Flags().BoolVar(&noWeb, "no-web", false, "不生成前端代码")
generateCmd.Flags().BoolVar(&noServer, "no-server", false, "不生成后端代码")
generateCmd.Flags().BoolVar(&onlyTemplate, "only-template", false, "仅生成模板(不注入代码到现有文件)")
generateCmd.Flags().BoolVar(&isAdd, "add", false, "追加模式(不覆盖已有文件)")
generateCmd.Flags().BoolVar(&isTree, "tree", false, "树形结构数据")
generateCmd.Flags().StringVar(&treeJson, "tree-json", "", "树形结构展示字段")
}
// AutoCodeConfig represents the configuration for code generation
type AutoCodeConfig struct {
Package string `json:"package" yaml:"package"`
StructName string `json:"structName" yaml:"structName"`
TableName string `json:"tableName" yaml:"tableName"`
Description string `json:"description" yaml:"description"`
BusinessDB string `json:"businessDB" yaml:"businessDB"`
Abbreviation string `json:"abbreviation" yaml:"abbreviation"`
Options AutoCodeOptions `json:"options" yaml:"options"`
Fields []AutoCodeField `json:"fields" yaml:"fields"`
}
// AutoCodeOptions represents generation options
type AutoCodeOptions struct {
GvaModel bool `json:"gvaModel" yaml:"gvaModel"` // 使用 KRA_MODEL (包含 ID, CreatedAt, UpdatedAt, DeletedAt)
AutoMigrate bool `json:"autoMigrate" yaml:"autoMigrate"` // 自动迁移数据库表结构
AutoCreateResource bool `json:"autoCreateResource" yaml:"autoCreateResource"` // 自动创建资源标识
AutoCreateApiToSql bool `json:"autoCreateApiToSql" yaml:"autoCreateApiToSql"` // 自动创建 API 记录
AutoCreateMenuToSql bool `json:"autoCreateMenuToSql" yaml:"autoCreateMenuToSql"` // 自动创建菜单记录
AutoCreateBtnAuth bool `json:"autoCreateBtnAuth" yaml:"autoCreateBtnAuth"` // 自动创建按钮权限
OnlyTemplate bool `json:"onlyTemplate" yaml:"onlyTemplate"` // 仅生成模板(不注入代码)
IsTree bool `json:"isTree" yaml:"isTree"` // 树形结构
TreeJson string `json:"treeJson" yaml:"treeJson"` // 树形结构展示字段
IsAdd bool `json:"isAdd" yaml:"isAdd"` // 追加模式(不覆盖已有文件)
GenerateWeb bool `json:"generateWeb" yaml:"generateWeb"` // 生成前端代码
GenerateServer bool `json:"generateServer" yaml:"generateServer"` // 生成后端代码
}
// DataSource 数据源配置
type DataSource struct {
DBName string `json:"dbName" yaml:"dbName"`
Table string `json:"table" yaml:"table"`
Label string `json:"label" yaml:"label"`
Value string `json:"value" yaml:"value"`
Association int `json:"association" yaml:"association"` // 关联关系 1 一对一 2 一对多
HasDeletedAt bool `json:"hasDeletedAt" yaml:"hasDeletedAt"`
}
// AutoCodeField represents a field configuration
type AutoCodeField struct {
FieldName string `json:"fieldName" yaml:"fieldName"`
FieldDesc string `json:"fieldDesc" yaml:"fieldDesc"`
FieldType string `json:"fieldType" yaml:"fieldType"`
FieldJson string `json:"fieldJson" yaml:"fieldJson"`
ColumnName string `json:"columnName" yaml:"columnName"`
DataTypeLong string `json:"dataTypeLong" yaml:"dataTypeLong"`
Form bool `json:"form" yaml:"form"`
Table bool `json:"table" yaml:"table"`
Desc bool `json:"desc" yaml:"desc"` // 是否前端详情
Excel bool `json:"excel" yaml:"excel"` // 是否导入/导出
Require bool `json:"require" yaml:"require"`
DefaultValue string `json:"defaultValue" yaml:"defaultValue"` // 默认值
ErrorText string `json:"errorText" yaml:"errorText"` // 校验失败文字
Clearable bool `json:"clearable" yaml:"clearable"` // 是否可清空
Sort bool `json:"sort" yaml:"sort"` // 是否增加排序
FieldSearchType string `json:"fieldSearchType" yaml:"fieldSearchType"`
FieldSearchHide bool `json:"fieldSearchHide" yaml:"fieldSearchHide"` // 是否隐藏查询条件
DictType string `json:"dictType" yaml:"dictType"`
DataSource *DataSource `json:"dataSource" yaml:"dataSource"` // 数据源
FieldIndexType string `json:"fieldIndexType" yaml:"fieldIndexType"` // 索引类型
PrimaryKey bool `json:"primaryKey" yaml:"primaryKey"` // 是否主键
}
func runGenerate(cmd *cobra.Command, args []string) {
var config *AutoCodeConfig
var err error
// Determine configuration source
if configFile != "" {
config, err = loadConfigFile(configFile)
if err != nil {
fmt.Fprintf(os.Stderr, "加载配置文件失败: %v\n", err)
os.Exit(1)
}
} else if interactive {
config, err = runInteractiveMode()
if err != nil {
fmt.Fprintf(os.Stderr, "交互模式错误: %v\n", err)
os.Exit(1)
}
} else if hasRequiredFlags() {
config = buildConfigFromFlags()
} else {
fmt.Println("请指定配置文件 (-c) 或使用交互模式 (-i) 或提供必要参数")
fmt.Println("使用 'kra-gen generate --help' 查看帮助")
os.Exit(1)
}
// Apply command line overrides
applyFlagOverrides(config)
// Validate configuration
if err := validateConfig(config); err != nil {
fmt.Fprintf(os.Stderr, "配置验证失败: %v\n", err)
os.Exit(1)
}
// Execute generation
if preview {
if err := executePreview(config); err != nil {
fmt.Fprintf(os.Stderr, "预览失败: %v\n", err)
os.Exit(1)
}
} else {
if err := executeGenerate(config); err != nil {
fmt.Fprintf(os.Stderr, "生成失败: %v\n", err)
os.Exit(1)
}
}
}
// loadConfigFile loads configuration from a YAML or JSON file
func loadConfigFile(path string) (*AutoCodeConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
config := &AutoCodeConfig{}
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".yaml", ".yml":
if err := yaml.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("解析 YAML 配置失败: %w", err)
}
case ".json":
if err := json.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("解析 JSON 配置失败: %w", err)
}
default:
return nil, fmt.Errorf("不支持的配置文件格式: %s", ext)
}
return config, nil
}
// hasRequiredFlags checks if required command line flags are provided
func hasRequiredFlags() bool {
return tableName != "" && packageName != "" && structName != ""
}
// buildConfigFromFlags builds configuration from command line flags
func buildConfigFromFlags() *AutoCodeConfig {
abbreviation := strings.ToLower(structName)
if len(abbreviation) > 0 {
abbreviation = strings.ToLower(string(structName[0])) + structName[1:]
}
return &AutoCodeConfig{
Package: packageName,
StructName: structName,
TableName: tableName,
Description: desc,
BusinessDB: dbName,
Abbreviation: abbreviation,
Options: AutoCodeOptions{
GvaModel: true,
AutoMigrate: true,
AutoCreateResource: false,
AutoCreateApiToSql: true,
AutoCreateMenuToSql: true,
AutoCreateBtnAuth: true,
OnlyTemplate: false,
IsTree: false,
TreeJson: "",
IsAdd: false,
GenerateWeb: !noWeb,
GenerateServer: !noServer,
},
}
}
// applyFlagOverrides applies command line flag overrides to the configuration
func applyFlagOverrides(config *AutoCodeConfig) {
if noWeb {
config.Options.GenerateWeb = false
}
if noServer {
config.Options.GenerateServer = false
}
if onlyTemplate {
config.Options.OnlyTemplate = true
}
if isAdd {
config.Options.IsAdd = true
}
if isTree {
config.Options.IsTree = true
}
if treeJson != "" {
config.Options.TreeJson = treeJson
}
}
// validateConfig validates the configuration
func validateConfig(config *AutoCodeConfig) error {
if config.Package == "" {
return fmt.Errorf("包名不能为空")
}
if config.StructName == "" {
return fmt.Errorf("结构体名不能为空")
}
if config.TableName == "" {
return fmt.Errorf("表名不能为空")
}
return nil
}
// executePreview executes preview mode
func executePreview(config *AutoCodeConfig) error {
fmt.Println("========================================")
fmt.Println("代码预览模式")
fmt.Println("========================================")
fmt.Printf("包名: %s\n", config.Package)
fmt.Printf("结构体: %s\n", config.StructName)
fmt.Printf("表名: %s\n", config.TableName)
fmt.Printf("描述: %s\n", config.Description)
fmt.Printf("数据库: %s\n", config.BusinessDB)
fmt.Println("----------------------------------------")
// 显示生成选项
fmt.Println("生成选项:")
fmt.Printf(" 使用 KRA_MODEL: %s\n", boolToStr(config.Options.GvaModel))
fmt.Printf(" 自动迁移: %s\n", boolToStr(config.Options.AutoMigrate))
fmt.Printf(" 生成后端: %s\n", boolToStr(config.Options.GenerateServer))
fmt.Printf(" 生成前端: %s\n", boolToStr(config.Options.GenerateWeb))
fmt.Printf(" 仅生成模板: %s\n", boolToStr(config.Options.OnlyTemplate))
fmt.Printf(" 追加模式: %s\n", boolToStr(config.Options.IsAdd))
if !config.Options.OnlyTemplate {
fmt.Printf(" 创建 API 记录: %s\n", boolToStr(config.Options.AutoCreateApiToSql))
fmt.Printf(" 创建菜单记录: %s\n", boolToStr(config.Options.AutoCreateMenuToSql))
fmt.Printf(" 创建按钮权限: %s\n", boolToStr(config.Options.AutoCreateBtnAuth))
fmt.Printf(" 创建资源标识: %s\n", boolToStr(config.Options.AutoCreateResource))
}
if config.Options.IsTree {
fmt.Printf(" 树形结构: 是 (展示字段: %s)\n", config.Options.TreeJson)
}
fmt.Println("----------------------------------------")
fmt.Println("将生成以下文件:")
fmt.Println()
workDir := getWorkingDir()
if config.Options.GenerateServer {
fmt.Println("后端文件:")
fmt.Printf(" - %s/internal/biz/%s/%s.go\n", workDir, config.Package, strings.ToLower(config.StructName))
fmt.Printf(" - %s/internal/data/%s/%s.go\n", workDir, config.Package, strings.ToLower(config.StructName))
fmt.Printf(" - %s/internal/data/model/%s/%s.go\n", workDir, config.Package, strings.ToLower(config.StructName))
fmt.Printf(" - %s/internal/service/%s/%s.go\n", workDir, config.Package, strings.ToLower(config.StructName))
fmt.Printf(" - %s/internal/service/types/%s/request/%s.go\n", workDir, config.Package, strings.ToLower(config.StructName))
fmt.Printf(" - %s/internal/server/handler/%s/%s.go\n", workDir, config.Package, strings.ToLower(config.StructName))
fmt.Printf(" - %s/internal/server/router/%s/%s.go\n", workDir, config.Package, strings.ToLower(config.StructName))
}
if config.Options.GenerateWeb {
fmt.Println()
fmt.Println("前端文件:")
fmt.Printf(" - %s/web/src/pages/%s/%s/index.tsx\n", workDir, config.Package, strings.ToLower(config.StructName))
fmt.Printf(" - %s/web/src/pages/%s/%s/form.tsx\n", workDir, config.Package, strings.ToLower(config.StructName))
fmt.Printf(" - %s/web/src/pages/%s/%s/detail.tsx\n", workDir, config.Package, strings.ToLower(config.StructName))
fmt.Printf(" - %s/web/src/services/kratos/%s.ts\n", workDir, config.Package)
fmt.Printf(" - %s/web/src/types/%s/%s.d.ts\n", workDir, config.Package, strings.ToLower(config.StructName))
}
fmt.Println()
fmt.Println("字段配置:")
if len(config.Fields) == 0 {
fmt.Println(" (未配置字段,将从数据库表结构读取)")
} else {
for _, field := range config.Fields {
searchInfo := ""
if field.FieldSearchType != "" {
searchInfo = fmt.Sprintf(" [搜索:%s]", field.FieldSearchType)
}
dictInfo := ""
if field.DictType != "" {
dictInfo = fmt.Sprintf(" [字典:%s]", field.DictType)
}
fmt.Printf(" - %s (%s): %s%s%s\n", field.FieldName, field.FieldType, field.FieldDesc, searchInfo, dictInfo)
}
}
fmt.Println()
fmt.Println("========================================")
fmt.Println("预览完成,未写入任何文件")
fmt.Println("========================================")
return nil
}
// boolToStr 将布尔值转换为是/否字符串
func boolToStr(b bool) string {
if b {
return "是"
}
return "否"
}
// executeGenerate executes the actual code generation
func executeGenerate(config *AutoCodeConfig) error {
fmt.Println("========================================")
fmt.Println("开始生成代码...")
fmt.Println("========================================")
fmt.Printf("包名: %s\n", config.Package)
fmt.Printf("结构体: %s\n", config.StructName)
fmt.Printf("表名: %s\n", config.TableName)
fmt.Println("----------------------------------------")
// TODO: 实现实际的代码生成逻辑
// 这里需要调用 AutoCodeTemplateUsecase 的 Create 方法
// 由于 CLI 工具需要独立运行,需要初始化数据库连接和依赖
fmt.Println()
fmt.Println("注意: 完整的代码生成功能需要连接数据库。")
fmt.Println("请使用 Web 界面进行代码生成,或确保配置文件包含数据库连接信息。")
fmt.Println()
fmt.Println("========================================")
fmt.Println("代码生成完成!")
fmt.Println("========================================")
return nil
}
// runInteractiveMode runs the interactive configuration mode
func runInteractiveMode() (*AutoCodeConfig, error) {
// 使用新的交互模式实现,支持数据库/表选择
return runInteractiveModeWithDB()
}
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
// Package main provides the CLI entry point for KRA code generation tool.
// This tool supports generating Kratos DDD architecture backend code and React frontend code.
package main
import (
"os"
)
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
+608
View File
@@ -0,0 +1,608 @@
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/fatih/color"
"github.com/spf13/cobra"
"kra/pkg/utils/autocode"
)
// previewCmd represents the preview command
var previewCmd = &cobra.Command{
Use: "preview",
Short: "预览生成的代码",
Long: `预览将要生成的代码,不写入任何文件。
这是 'generate -p' 的快捷方式。
使用示例:
# 从配置文件预览
kra-gen preview -c autocode.yaml
# 使用命令行参数预览
kra-gen preview --table users --package example --struct User
# 预览时显示完整代码内容
kra-gen preview -c autocode.yaml --full
# 预览时不使用语法高亮
kra-gen preview -c autocode.yaml --full --no-color`,
Run: runPreview,
}
var (
// Preview specific flags
fullPreview bool
noColor bool
)
func init() {
// Preview command specific flags (same as generate)
previewCmd.Flags().StringVar(&dbName, "db", "", "数据库连接名")
previewCmd.Flags().StringVar(&tableName, "table", "", "表名")
previewCmd.Flags().StringVar(&packageName, "package", "", "包名")
previewCmd.Flags().StringVar(&structName, "struct", "", "结构体名")
previewCmd.Flags().StringVar(&desc, "desc", "", "描述")
previewCmd.Flags().BoolVar(&noWeb, "no-web", false, "不生成前端代码")
previewCmd.Flags().BoolVar(&noServer, "no-server", false, "不生成后端代码")
previewCmd.Flags().BoolVar(&fullPreview, "full", false, "显示完整代码内容")
previewCmd.Flags().BoolVar(&noColor, "no-color", false, "不使用语法高亮")
}
func runPreview(cmd *cobra.Command, args []string) {
var config *AutoCodeConfig
var err error
// Determine configuration source
if configFile != "" {
config, err = loadConfigFile(configFile)
if err != nil {
fmt.Fprintf(os.Stderr, "加载配置文件失败: %v\n", err)
os.Exit(1)
}
} else if hasRequiredFlags() {
config = buildConfigFromFlags()
} else {
fmt.Println("请指定配置文件 (-c) 或提供必要参数 (--table, --package, --struct)")
fmt.Println("使用 'kra-gen preview --help' 查看帮助")
os.Exit(1)
}
// Apply command line overrides
applyFlagOverrides(config)
// Validate configuration
if err := validateConfig(config); err != nil {
fmt.Fprintf(os.Stderr, "配置验证失败: %v\n", err)
os.Exit(1)
}
// Execute preview
if fullPreview {
if err := executeFullPreview(config); err != nil {
fmt.Fprintf(os.Stderr, "预览失败: %v\n", err)
os.Exit(1)
}
} else {
if err := executePreview(config); err != nil {
fmt.Fprintf(os.Stderr, "预览失败: %v\n", err)
os.Exit(1)
}
}
}
// TemplateInfo 模板信息
type TemplateInfo struct {
Name string // 模板名称
Path string // 模板路径
OutputPath string // 输出路径
Type string // 类型: server/web
Layer string // 层级: biz/data/handler/service/router/pages/services/types
}
// getTemplateList 获取模板列表
func getTemplateList(config *AutoCodeConfig) []TemplateInfo {
var templates []TemplateInfo
workDir := getWorkingDir()
pkg := config.Package
structLower := strings.ToLower(config.StructName)
if config.Options.GenerateServer {
// 后端模板
templates = append(templates, []TemplateInfo{
{
Name: "Biz Layer",
Path: "server/biz/biz.go.tpl",
OutputPath: filepath.Join(workDir, "internal/biz", pkg, structLower+".go"),
Type: "server",
Layer: "biz",
},
{
Name: "Biz Enter",
Path: "server/biz/enter.go.tpl",
OutputPath: filepath.Join(workDir, "internal/biz", pkg, "enter.go"),
Type: "server",
Layer: "biz",
},
{
Name: "Data Layer",
Path: "server/data/data.go.tpl",
OutputPath: filepath.Join(workDir, "internal/data", pkg, structLower+".go"),
Type: "server",
Layer: "data",
},
{
Name: "Data Enter",
Path: "server/data/enter.go.tpl",
OutputPath: filepath.Join(workDir, "internal/data", pkg, "enter.go"),
Type: "server",
Layer: "data",
},
{
Name: "Data Model",
Path: "server/data/model.go.tpl",
OutputPath: filepath.Join(workDir, "internal/data/model", pkg, structLower+".go"),
Type: "server",
Layer: "model",
},
{
Name: "Handler Layer",
Path: "server/handler/handler.go.tpl",
OutputPath: filepath.Join(workDir, "internal/server/handler", pkg, structLower+".go"),
Type: "server",
Layer: "handler",
},
{
Name: "Handler Enter",
Path: "server/handler/enter.go.tpl",
OutputPath: filepath.Join(workDir, "internal/server/handler", pkg, "enter.go"),
Type: "server",
Layer: "handler",
},
{
Name: "Service Layer",
Path: "server/service/service.go.tpl",
OutputPath: filepath.Join(workDir, "internal/service", pkg, structLower+".go"),
Type: "server",
Layer: "service",
},
{
Name: "Service Enter",
Path: "server/service/enter.go.tpl",
OutputPath: filepath.Join(workDir, "internal/service", pkg, "enter.go"),
Type: "server",
Layer: "service",
},
{
Name: "Request Types",
Path: "server/types/request.go.tpl",
OutputPath: filepath.Join(workDir, "internal/service/types", pkg, "request", structLower+".go"),
Type: "server",
Layer: "types",
},
{
Name: "Router Layer",
Path: "server/router/router.go.tpl",
OutputPath: filepath.Join(workDir, "internal/server/router", pkg, structLower+".go"),
Type: "server",
Layer: "router",
},
{
Name: "Router Enter",
Path: "server/router/enter.go.tpl",
OutputPath: filepath.Join(workDir, "internal/server/router", pkg, "enter.go"),
Type: "server",
Layer: "router",
},
}...)
}
if config.Options.GenerateWeb {
// 前端模板
templates = append(templates, []TemplateInfo{
{
Name: "React Page",
Path: "web/pages/index.tsx.tpl",
OutputPath: filepath.Join(workDir, "web/src/pages", pkg, structLower, "index.tsx"),
Type: "web",
Layer: "pages",
},
{
Name: "React Form",
Path: "web/pages/form.tsx.tpl",
OutputPath: filepath.Join(workDir, "web/src/pages", pkg, structLower, "form.tsx"),
Type: "web",
Layer: "pages",
},
{
Name: "React Detail",
Path: "web/pages/detail.tsx.tpl",
OutputPath: filepath.Join(workDir, "web/src/pages", pkg, structLower, "detail.tsx"),
Type: "web",
Layer: "pages",
},
{
Name: "API Service",
Path: "web/services/api.ts.tpl",
OutputPath: filepath.Join(workDir, "web/src/services/kratos", pkg+".ts"),
Type: "web",
Layer: "services",
},
{
Name: "TypeScript Types",
Path: "web/types/types.d.ts.tpl",
OutputPath: filepath.Join(workDir, "web/src/types", pkg, structLower+".d.ts"),
Type: "web",
Layer: "types",
},
}...)
}
return templates
}
// buildTemplateData 构建模板数据
func buildTemplateData(config *AutoCodeConfig) map[string]interface{} {
// 转换字段
fields := make([]autocode.AutoCodeField, len(config.Fields))
for i, f := range config.Fields {
var ds *autocode.DataSource
if f.DataSource != nil {
ds = &autocode.DataSource{
DBName: f.DataSource.DBName,
Table: f.DataSource.Table,
Label: f.DataSource.Label,
Value: f.DataSource.Value,
Association: f.DataSource.Association,
HasDeletedAt: f.DataSource.HasDeletedAt,
}
}
fields[i] = autocode.AutoCodeField{
FieldName: f.FieldName,
FieldDesc: f.FieldDesc,
FieldType: f.FieldType,
FieldJson: f.FieldJson,
DataTypeLong: f.DataTypeLong,
ColumnName: f.ColumnName,
FieldSearchType: f.FieldSearchType,
FieldSearchHide: f.FieldSearchHide,
DictType: f.DictType,
Form: f.Form,
Table: f.Table,
Desc: f.Desc,
Excel: f.Excel,
Require: f.Require,
DefaultValue: f.DefaultValue,
ErrorText: f.ErrorText,
Clearable: f.Clearable,
Sort: f.Sort,
PrimaryKey: f.PrimaryKey,
DataSource: ds,
FieldIndexType: f.FieldIndexType,
}
}
// 查找主键字段
var primaryField autocode.AutoCodeField
for _, f := range fields {
if f.PrimaryKey {
primaryField = f
break
}
}
// 如果没有主键,使用默认的 ID
if primaryField.FieldName == "" {
primaryField = autocode.AutoCodeField{
FieldName: "ID",
FieldJson: "ID",
FieldType: "uint",
}
}
// 收集字典类型
var dictTypes []string
hasDataSource := false
for _, f := range fields {
if f.DictType != "" {
dictTypes = append(dictTypes, f.DictType)
}
if f.DataSource != nil {
hasDataSource = true
}
}
// 检查特殊字段类型
hasTimer := false
hasPic := false
hasFile := false
hasRichText := false
hasArray := false
needSort := false
for _, f := range fields {
switch f.FieldType {
case "time.Time":
hasTimer = true
case "picture":
hasPic = true
case "file", "pictures":
hasFile = true
case "richtext":
hasRichText = true
case "array":
hasArray = true
}
if f.Sort {
needSort = true
}
}
// 获取模块名
module := "kra"
if modFile, err := os.ReadFile("go.mod"); err == nil {
lines := strings.Split(string(modFile), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "module ") {
module = strings.TrimSpace(strings.TrimPrefix(line, "module "))
break
}
}
}
return map[string]interface{}{
"Package": config.Package,
"PackageName": strings.ToLower(config.Package),
"PackageT": strings.Title(config.Package),
"StructName": config.StructName,
"TableName": config.TableName,
"Description": config.Description,
"BusinessDB": config.BusinessDB,
"Abbreviation": config.Abbreviation,
"HumpPackageName": strings.ToLower(string(config.Package[0])) + config.Package[1:],
"Module": module,
"Fields": fields,
"PrimaryField": primaryField,
"DictTypes": dictTypes,
"HasDataSource": hasDataSource,
"HasTimer": hasTimer,
"HasPic": hasPic,
"HasFile": hasFile,
"HasRichText": hasRichText,
"HasArray": hasArray,
"NeedSort": needSort,
"GvaModel": config.Options.GvaModel,
"AutoMigrate": config.Options.AutoMigrate,
"AutoCreateResource": config.Options.AutoCreateResource,
"OnlyTemplate": config.Options.OnlyTemplate,
"IsTree": config.Options.IsTree,
"TreeJson": config.Options.TreeJson,
"IsAdd": config.Options.IsAdd,
}
}
// findTemplateDir 查找模板目录
func findTemplateDir() string {
// 优先使用命令行指定的模板目录
if templateDir != "" {
return templateDir
}
// 尝试多个可能的路径
possiblePaths := []string{
"resource/package",
"../resource/package",
"../../resource/package",
}
// 获取可执行文件所在目录
if execPath, err := os.Executable(); err == nil {
execDir := filepath.Dir(execPath)
possiblePaths = append(possiblePaths,
filepath.Join(execDir, "resource/package"),
filepath.Join(execDir, "../resource/package"),
)
}
for _, p := range possiblePaths {
if _, err := os.Stat(p); err == nil {
return p
}
}
return "resource/package"
}
// renderTemplate 渲染单个模板
func renderTemplate(tplPath string, data map[string]interface{}) (string, error) {
// 查找模板目录
baseDir := findTemplateDir()
resourcePath := filepath.Join(baseDir, tplPath)
tplContent, err := os.ReadFile(resourcePath)
if err != nil {
return "", fmt.Errorf("读取模板文件失败 (%s): %w", resourcePath, err)
}
// 创建模板
tmpl, err := template.New(filepath.Base(tplPath)).
Funcs(autocode.GetTemplateFuncMap()).
Parse(string(tplContent))
if err != nil {
return "", fmt.Errorf("解析模板失败: %w", err)
}
// 渲染模板
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return "", fmt.Errorf("渲染模板失败: %w", err)
}
return buf.String(), nil
}
// executeFullPreview 执行完整预览(显示所有文件内容)
func executeFullPreview(config *AutoCodeConfig) error {
// 颜色定义
titleColor := color.New(color.FgCyan, color.Bold)
fileColor := color.New(color.FgGreen, color.Bold)
pathColor := color.New(color.FgYellow)
separatorColor := color.New(color.FgBlue)
errorColor := color.New(color.FgRed)
if noColor {
color.NoColor = true
}
// 打印头部信息
titleColor.Println("╔════════════════════════════════════════════════════════════════╗")
titleColor.Println("║ KRA 代码预览 (完整模式) ║")
titleColor.Println("╚════════════════════════════════════════════════════════════════╝")
fmt.Println()
// 打印配置信息
fmt.Printf("包名: %s\n", config.Package)
fmt.Printf("结构体: %s\n", config.StructName)
fmt.Printf("表名: %s\n", config.TableName)
fmt.Printf("描述: %s\n", config.Description)
fmt.Println()
// 获取模板列表
templates := getTemplateList(config)
// 构建模板数据
data := buildTemplateData(config)
// 统计信息
successCount := 0
failCount := 0
var failedTemplates []string
// 渲染并显示每个模板
for _, tpl := range templates {
separatorColor.Println("────────────────────────────────────────────────────────────────")
fileColor.Printf("📄 %s\n", tpl.Name)
pathColor.Printf(" 输出路径: %s\n", tpl.OutputPath)
fmt.Println()
content, err := renderTemplate(tpl.Path, data)
if err != nil {
errorColor.Printf(" ❌ 渲染失败: %v\n", err)
failCount++
failedTemplates = append(failedTemplates, tpl.Name)
continue
}
// 显示代码内容(带语法高亮)
printCodeWithHighlight(content, tpl.OutputPath)
successCount++
fmt.Println()
}
// 打印统计信息
separatorColor.Println("════════════════════════════════════════════════════════════════")
titleColor.Println("预览统计")
fmt.Printf(" 成功: %d 个文件\n", successCount)
if failCount > 0 {
errorColor.Printf(" 失败: %d 个文件\n", failCount)
for _, name := range failedTemplates {
errorColor.Printf(" - %s\n", name)
}
}
fmt.Println()
titleColor.Println("⚠️ 预览模式 - 未写入任何文件")
return nil
}
// printCodeWithHighlight 打印代码(带简单语法高亮)
func printCodeWithHighlight(content, filePath string) {
if noColor {
fmt.Println(content)
return
}
// 根据文件扩展名确定语言
ext := strings.ToLower(filepath.Ext(filePath))
// 简单的语法高亮
lines := strings.Split(content, "\n")
for i, line := range lines {
// 行号
lineNum := color.New(color.FgHiBlack).Sprintf("%4d │ ", i+1)
fmt.Print(lineNum)
// 根据语言类型进行简单高亮
switch ext {
case ".go":
fmt.Println(highlightGo(line))
case ".tsx", ".ts":
fmt.Println(highlightTypeScript(line))
default:
fmt.Println(line)
}
}
}
// highlightGo 简单的 Go 语法高亮
func highlightGo(line string) string {
if noColor {
return line
}
// 注释
if strings.HasPrefix(strings.TrimSpace(line), "//") {
return color.HiBlackString(line)
}
// 关键字
keywords := []string{"package", "import", "func", "type", "struct", "interface",
"return", "if", "else", "for", "range", "switch", "case", "default",
"var", "const", "map", "chan", "go", "defer", "select", "break", "continue"}
result := line
for _, kw := range keywords {
// 只替换完整单词
result = strings.ReplaceAll(result, " "+kw+" ", " "+color.CyanString(kw)+" ")
result = strings.ReplaceAll(result, "\t"+kw+" ", "\t"+color.CyanString(kw)+" ")
if strings.HasPrefix(result, kw+" ") {
result = color.CyanString(kw) + result[len(kw):]
}
}
return result
}
// highlightTypeScript 简单的 TypeScript 语法高亮
func highlightTypeScript(line string) string {
if noColor {
return line
}
// 注释
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*") {
return color.HiBlackString(line)
}
// 关键字
keywords := []string{"import", "export", "from", "const", "let", "var", "function",
"return", "if", "else", "for", "while", "switch", "case", "default",
"interface", "type", "class", "extends", "implements", "async", "await",
"try", "catch", "finally", "throw", "new", "this", "super"}
result := line
for _, kw := range keywords {
result = strings.ReplaceAll(result, " "+kw+" ", " "+color.CyanString(kw)+" ")
result = strings.ReplaceAll(result, "\t"+kw+" ", "\t"+color.CyanString(kw)+" ")
if strings.HasPrefix(result, kw+" ") {
result = color.CyanString(kw) + result[len(kw):]
}
}
return result
}
+202
View File
@@ -0,0 +1,202 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
var (
// Version information
version = "v1.0.0"
// Global flags
configFile string
outputDir string
templateDir string
verbose bool
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "kra-gen",
Short: "KRA 代码生成工具",
Long: `KRA 自动代码生成工具,支持生成 Kratos DDD 架构的后端代码和 React 前端代码。
功能特性:
- 生成符合 Kratos DDD 分层架构的代码(biz/data/service/server)
- 使用 GORM Gen 生成类型安全的数据库操作代码
- 生成 React TypeScript 前端代码
- 支持配置文件和命令行参数两种方式
- 支持预览模式,不写入文件
使用示例:
# 从配置文件生成代码
kra-gen generate -c autocode.yaml
# 交互模式
kra-gen generate -i
# 预览模式
kra-gen generate -c autocode.yaml -p
# 查看版本
kra-gen version`,
Run: func(cmd *cobra.Command, args []string) {
// If no subcommand is provided, show help
cmd.Help()
},
}
func init() {
// Global flags
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "配置文件路径 (JSON/YAML)")
rootCmd.PersistentFlags().StringVarP(&outputDir, "output", "o", "", "输出目录(默认当前项目)")
rootCmd.PersistentFlags().StringVarP(&templateDir, "template", "t", "", "模板目录(默认 resource/package)")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "显示详细输出")
// Add subcommands
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(generateCmd)
rootCmd.AddCommand(previewCmd)
rootCmd.AddCommand(listCmd)
}
// versionCmd represents the version command
var versionCmd = &cobra.Command{
Use: "version",
Short: "显示版本信息",
Long: "显示 KRA 代码生成工具的版本信息",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("kra-gen %s\n", version)
fmt.Println("KRA 自动代码生成工具")
fmt.Println("支持 Kratos DDD 架构 + GORM Gen + React TypeScript")
},
}
// listCmd represents the list command for listing available templates or packages
var listCmd = &cobra.Command{
Use: "list [templates|packages|databases]",
Short: "列出可用资源",
Long: `列出可用的模板、包或数据库连接。
使用示例:
kra-gen list templates # 列出可用模板
kra-gen list packages # 列出已创建的包
kra-gen list databases # 列出数据库连接`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
fmt.Println("请指定要列出的资源类型: templates, packages, databases")
return
}
switch args[0] {
case "templates":
listTemplates()
case "packages":
listPackages()
case "databases":
listDatabases()
default:
fmt.Printf("未知的资源类型: %s\n", args[0])
fmt.Println("可用类型: templates, packages, databases")
}
},
}
func listTemplates() {
fmt.Println("可用模板:")
fmt.Println(" package - 标准包模板 (Kratos DDD 架构)")
fmt.Println(" plugin - 插件模板 (独立功能模块)")
}
func listPackages() {
// TODO: 实现从数据库或文件系统读取已创建的包
fmt.Println("已创建的包:")
fmt.Println(" (需要连接数据库获取)")
}
func listDatabases() {
// 尝试从配置文件加载数据库连接
configPath := "configs/config.yaml"
if _, err := os.Stat(configPath); err != nil {
fmt.Println("数据库连接:")
fmt.Println(" 未找到配置文件 configs/config.yaml")
fmt.Println(" 请使用 'kra-gen generate -i' 进入交互模式配置数据库")
return
}
data, err := os.ReadFile(configPath)
if err != nil {
fmt.Printf("读取配置文件失败: %v\n", err)
return
}
var config struct {
Mysql struct {
Path string `yaml:"path"`
Port string `yaml:"port"`
DBName string `yaml:"db_name"`
Username string `yaml:"username"`
} `yaml:"mysql"`
Pgsql struct {
Path string `yaml:"path"`
Port string `yaml:"port"`
DBName string `yaml:"db_name"`
Username string `yaml:"username"`
} `yaml:"pgsql"`
Sqlite struct {
DBName string `yaml:"db_name"`
} `yaml:"sqlite"`
}
if err := yaml.Unmarshal(data, &config); err != nil {
fmt.Printf("解析配置文件失败: %v\n", err)
return
}
fmt.Println("数据库连接:")
found := false
if config.Mysql.Path != "" && config.Mysql.DBName != "" {
fmt.Printf(" MySQL: %s@%s:%s/%s\n", config.Mysql.Username, config.Mysql.Path, config.Mysql.Port, config.Mysql.DBName)
found = true
}
if config.Pgsql.Path != "" && config.Pgsql.DBName != "" {
fmt.Printf(" PostgreSQL: %s@%s:%s/%s\n", config.Pgsql.Username, config.Pgsql.Path, config.Pgsql.Port, config.Pgsql.DBName)
found = true
}
if config.Sqlite.DBName != "" {
fmt.Printf(" SQLite: %s\n", config.Sqlite.DBName)
found = true
}
if !found {
fmt.Println(" 未配置任何数据库连接")
}
}
// getWorkingDir returns the current working directory or the specified output directory
func getWorkingDir() string {
if outputDir != "" {
return outputDir
}
dir, err := os.Getwd()
if err != nil {
return "."
}
return dir
}
// getTemplateDir returns the template directory path
func getTemplateDir() string {
if templateDir != "" {
return templateDir
}
return "resource/package"
}