后端重构
This commit is contained in:
@@ -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 # 时间范围搜索
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user