后端重构

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
+1
View File
@@ -55,6 +55,7 @@ type CasbinRepo interface {
AddPolicies(rules [][]string) error
UpdateCasbin(adminAuthorityID, authorityId uint, casbinInfos []CasbinRule) error
RemoveFilteredPolicy(authorityId string) error
SyncPolicy(authorityId string, rules [][]string) error
}
// ApiUsecase API用例
+44 -11
View File
@@ -2,6 +2,8 @@ package system
import (
"context"
"github.com/go-kratos/kratos/v2/log"
)
// Db 数据库信息
@@ -23,34 +25,65 @@ type Column struct {
PrimaryKey int `json:"primaryKey" gorm:"column:primary_key"`
}
// DBInfo 数据库配置信息
type DBInfo struct {
AliasName string `json:"aliasName"`
DBName string `json:"dbName"`
Disable bool `json:"disable"`
DBType string `json:"dbtype"`
}
// AutoCodeRepo 自动代码仓储接口
type AutoCodeRepo interface {
GetDB(ctx context.Context) ([]Db, error)
GetTables(ctx context.Context, dbName string) ([]Table, error)
GetColumn(ctx context.Context, tableName, dbName string) ([]Column, error)
GetDB(ctx context.Context, businessDB string) ([]Db, error)
GetTables(ctx context.Context, businessDB, dbName string) ([]Table, error)
GetColumn(ctx context.Context, businessDB, tableName, dbName string) ([]Column, error)
GetDBList() []DBInfo
GetActiveDBName(businessDB string) string
}
// AutoCodeUsecase 自动代码用例
type AutoCodeUsecase struct {
repo AutoCodeRepo
llm *AutoCodeLLMUsecase
log *log.Helper
}
// NewAutoCodeUsecase 创建自动代码用例
func NewAutoCodeUsecase(repo AutoCodeRepo) *AutoCodeUsecase {
return &AutoCodeUsecase{repo: repo}
func NewAutoCodeUsecase(repo AutoCodeRepo, llm *AutoCodeLLMUsecase, logger log.Logger) *AutoCodeUsecase {
return &AutoCodeUsecase{
repo: repo,
llm: llm,
log: log.NewHelper(logger),
}
}
// GetDB 获取所有数据库
func (uc *AutoCodeUsecase) GetDB(ctx context.Context) ([]Db, error) {
return uc.repo.GetDB(ctx)
func (uc *AutoCodeUsecase) GetDB(ctx context.Context, businessDB string) ([]Db, error) {
return uc.repo.GetDB(ctx, businessDB)
}
// GetTables 获取指定数据库的所有表
func (uc *AutoCodeUsecase) GetTables(ctx context.Context, dbName string) ([]Table, error) {
return uc.repo.GetTables(ctx, dbName)
func (uc *AutoCodeUsecase) GetTables(ctx context.Context, businessDB, dbName string) ([]Table, error) {
return uc.repo.GetTables(ctx, businessDB, dbName)
}
// GetColumn 获取指定表的所有列
func (uc *AutoCodeUsecase) GetColumn(ctx context.Context, tableName, dbName string) ([]Column, error) {
return uc.repo.GetColumn(ctx, tableName, dbName)
func (uc *AutoCodeUsecase) GetColumn(ctx context.Context, businessDB, tableName, dbName string) ([]Column, error) {
return uc.repo.GetColumn(ctx, businessDB, tableName, dbName)
}
// GetDBList 获取配置的数据库列表
func (uc *AutoCodeUsecase) GetDBList() []DBInfo {
return uc.repo.GetDBList()
}
// GetActiveDBName 获取当前活动的数据库名
func (uc *AutoCodeUsecase) GetActiveDBName(businessDB string) string {
return uc.repo.GetActiveDBName(businessDB)
}
// LLMAuto 大模型自动生成代码
func (uc *AutoCodeUsecase) LLMAuto(ctx context.Context, llm map[string]interface{}) (interface{}, error) {
return uc.llm.LLMAuto(ctx, llm)
}
+92
View File
@@ -0,0 +1,92 @@
package system
import (
"context"
"go/ast"
"io"
)
// SysApi API实体
type SysApi struct {
ID uint `json:"ID" gorm:"primarykey"`
Path string `json:"path" gorm:"comment:api路径"`
Description string `json:"description" gorm:"comment:api中文描述"`
ApiGroup string `json:"apiGroup" gorm:"comment:api组"`
Method string `json:"method" gorm:"default:POST;comment:方法"`
}
func (SysApi) TableName() string {
return "sys_apis"
}
// Meta 菜单元信息
type Meta struct {
Title string `json:"title" gorm:"comment:菜单名"`
Icon string `json:"icon" gorm:"comment:菜单图标"`
DefaultMenu bool `json:"defaultMenu" gorm:"comment:是否是基础路由(开发中)"`
CloseTab bool `json:"closeTab" gorm:"comment:自动关闭tab"`
KeepAlive bool `json:"keepAlive" gorm:"comment:是否缓存"`
}
// SysBaseMenu 菜单实体
type SysBaseMenu struct {
ID uint `json:"ID" gorm:"primarykey"`
ParentId uint `json:"parentId" gorm:"comment:父菜单ID"`
Path string `json:"path" gorm:"comment:路由path"`
Name string `json:"name" gorm:"comment:路由name"`
Hidden bool `json:"hidden" gorm:"comment:是否在列表隐藏"`
Component string `json:"component" gorm:"comment:对应前端文件路径"`
Sort int `json:"sort" gorm:"comment:排序标记"`
Meta Meta `json:"meta" gorm:"embedded;comment:附加属性"`
MenuBtn []SysBaseMenuBtn `json:"menuBtn" gorm:"-"`
Parameters []SysBaseMenuParameter `json:"parameters" gorm:"-"`
}
func (SysBaseMenu) TableName() string {
return "sys_base_menus"
}
// SysBaseMenuBtn 菜单按钮
type SysBaseMenuBtn struct {
ID uint `json:"ID" gorm:"primarykey"`
SysBaseMenuID uint `json:"sysBaseMenuID" gorm:"comment:菜单ID"`
Name string `json:"name" gorm:"comment:按钮关键key"`
Desc string `json:"desc" gorm:"comment:按钮备注"`
}
func (SysBaseMenuBtn) TableName() string {
return "sys_base_menu_btns"
}
// SysBaseMenuParameter 菜单参数
type SysBaseMenuParameter struct {
ID uint `json:"ID" gorm:"primarykey"`
SysBaseMenuID uint `json:"sysBaseMenuID" gorm:"comment:菜单ID"`
Type string `json:"type" gorm:"comment:地址栏携带参数为params还是query"`
Key string `json:"key" gorm:"comment:地址栏携带参数的key"`
Value string `json:"value" gorm:"comment:地址栏携带参数的值"`
}
func (SysBaseMenuParameter) TableName() string {
return "sys_base_menu_parameters"
}
// Ast AST接口定义
type Ast interface {
Parse(filename string, writer io.Writer) (file *ast.File, err error)
Rollback(file *ast.File) error
Injection(file *ast.File) error
Format(filename string, writer io.Writer, file *ast.File) error
}
// AutoCodeHistoryRepo 扩展接口(添加Create方法)
type AutoCodeHistoryRepoExt interface {
AutoCodeHistoryRepo
Create(ctx context.Context, history *SysAutoCodeHistory) error
}
// AutoCodePackageRepoExt 扩展接口
// 注意:Templates方法已在AutoCodePackageRepo中定义,此处不再重复
type AutoCodePackageRepoExt interface {
AutoCodePackageRepo
}
+12
View File
@@ -46,12 +46,14 @@ type SysAutoHistoryRollBack struct {
// AutoCodeHistoryRepo 自动代码历史仓储接口
type AutoCodeHistoryRepo interface {
Create(ctx context.Context, history *SysAutoCodeHistory) error
First(ctx context.Context, id uint) (string, error)
Delete(ctx context.Context, id uint) error
GetList(ctx context.Context, page, pageSize int) ([]SysAutoCodeHistory, int64, error)
GetByID(ctx context.Context, id uint) (*SysAutoCodeHistory, error)
UpdateFlag(ctx context.Context, id uint, flag int) error
DropTable(ctx context.Context, businessDB, tableName string) error
Repeat(ctx context.Context, businessDB, structName, abbreviation, pkg string) bool
}
// AutoCodeHistoryUsecase 自动代码历史用例
@@ -64,11 +66,21 @@ func NewAutoCodeHistoryUsecase(repo AutoCodeHistoryRepo) *AutoCodeHistoryUsecase
return &AutoCodeHistoryUsecase{repo: repo}
}
// Create 创建历史记录
func (uc *AutoCodeHistoryUsecase) Create(ctx context.Context, history *SysAutoCodeHistory) error {
return uc.repo.Create(ctx, history)
}
// First 根据id获取meta信息
func (uc *AutoCodeHistoryUsecase) First(ctx context.Context, id uint) (string, error) {
return uc.repo.First(ctx, id)
}
// Repeat 检测重复
func (uc *AutoCodeHistoryUsecase) Repeat(ctx context.Context, businessDB, structName, abbreviation, pkg string) bool {
return uc.repo.Repeat(ctx, businessDB, structName, abbreviation, pkg)
}
// Delete 删除历史记录
func (uc *AutoCodeHistoryUsecase) Delete(ctx context.Context, id uint) error {
return uc.repo.Delete(ctx, id)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
package system
import "context"
// AutoCodeAdapter 自动代码数据库适配器接口
type AutoCodeAdapter interface {
// GetDB 获取所有数据库
GetDB(ctx context.Context) ([]Db, error)
// GetTables 获取指定数据库的所有表
GetTables(ctx context.Context, dbName string) ([]Table, error)
// GetColumn 获取指定表的所有列
GetColumn(ctx context.Context, tableName, dbName string) ([]Column, error)
}
// AutoCodeAdapterFactory 适配器工厂
type AutoCodeAdapterFactory struct {
adapters map[string]AutoCodeAdapter
}
// NewAutoCodeAdapterFactory 创建适配器工厂
func NewAutoCodeAdapterFactory() *AutoCodeAdapterFactory {
return &AutoCodeAdapterFactory{
adapters: make(map[string]AutoCodeAdapter),
}
}
// Register 注册适配器
func (f *AutoCodeAdapterFactory) Register(dbType string, adapter AutoCodeAdapter) {
f.adapters[dbType] = adapter
}
// Get 获取适配器
func (f *AutoCodeAdapterFactory) Get(dbType string) AutoCodeAdapter {
return f.adapters[dbType]
}
+84
View File
@@ -0,0 +1,84 @@
package system
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/go-kratos/kratos/v2/log"
"kra/internal/conf"
)
// AutoCodeLLMUsecase LLM用例
type AutoCodeLLMUsecase struct {
config *conf.AutoCodeLLMConfig
log *log.Helper
}
// NewAutoCodeLLMUsecase 创建LLM用例
func NewAutoCodeLLMUsecase(config *conf.AutoCodeLLMConfig, logger log.Logger) *AutoCodeLLMUsecase {
return &AutoCodeLLMUsecase{
config: config,
log: log.NewHelper(logger),
}
}
// LLMResponse LLM响应结构
type LLMResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// LLMAuto 调用大模型服务,返回生成结果数据
// 入参为通用 JSONMap,需包含 mode(例如 ai/butler/eye/painter 等)以及业务 prompt/payload
func (uc *AutoCodeLLMUsecase) LLMAuto(ctx context.Context, llm map[string]interface{}) (interface{}, error) {
if uc.config.AiPath == "" {
return nil, errors.New("请先前往插件市场个人中心获取AiPath并填入配置文件中")
}
// 构建调用路径:{AiPath} 中的 {FUNC} 由 mode 替换
mode := fmt.Sprintf("%v", llm["mode"])
path := strings.ReplaceAll(uc.config.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", mode))
// 构建请求体
body, err := json.Marshal(llm)
if err != nil {
return nil, fmt.Errorf("序列化请求失败: %w", err)
}
// 发送HTTP请求
req, err := http.NewRequestWithContext(ctx, "POST", path, strings.NewReader(string(body)))
if err != nil {
return nil, fmt.Errorf("创建请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("大模型生成失败: %w", err)
}
defer res.Body.Close()
// 读取响应
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("读取大模型响应失败: %w", err)
}
var resStruct LLMResponse
if err = json.Unmarshal(b, &resStruct); err != nil {
return nil, fmt.Errorf("解析大模型响应失败: %w", err)
}
if resStruct.Code == 7 { // 业务约定:7 表示模型生成失败
return nil, fmt.Errorf("大模型生成失败: %s", resStruct.Msg)
}
return resStruct.Data, nil
}
+67
View File
@@ -0,0 +1,67 @@
package system
import (
"context"
"os"
"path/filepath"
"text/template"
"github.com/go-kratos/kratos/v2/log"
"kra/internal/conf"
"kra/pkg/utils"
"kra/pkg/utils/autocode"
)
// AutoMcpTool MCP工具请求
type AutoMcpTool struct {
Name string `json:"name"` // 工具名称
Description string `json:"description"` // 工具描述
Parameters string `json:"parameters"` // 参数定义
}
// AutoCodeMCPUsecase MCP用例
type AutoCodeMCPUsecase struct {
config *conf.AutoCodeMCPConfig
log *log.Helper
}
// NewAutoCodeMCPUsecase 创建MCP用例
func NewAutoCodeMCPUsecase(config *conf.AutoCodeMCPConfig, logger log.Logger) *AutoCodeMCPUsecase {
return &AutoCodeMCPUsecase{
config: config,
log: log.NewHelper(logger),
}
}
// CreateMcp 创建MCP工具文件
func (uc *AutoCodeMCPUsecase) CreateMcp(ctx context.Context, info AutoMcpTool) (toolFilePath string, err error) {
mcpTemplatePath := filepath.Join(uc.config.Root, uc.config.Server, "resource", "mcp", "tools.tpl")
mcpToolPath := filepath.Join(uc.config.Root, uc.config.Server, "mcp")
var files *template.Template
templateName := filepath.Base(mcpTemplatePath)
files, err = template.New(templateName).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(mcpTemplatePath)
if err != nil {
return "", err
}
fileName := utils.HumpToUnderscore(info.Name)
toolFilePath = filepath.Join(mcpToolPath, fileName+".go")
f, err := os.Create(toolFilePath)
if err != nil {
return "", err
}
defer f.Close()
// 执行模板,将内容写入文件
err = files.Execute(f, info)
if err != nil {
return "", err
}
return toolFilePath, nil
}
+72
View File
@@ -0,0 +1,72 @@
package system
import (
"context"
"fmt"
"gorm.io/gorm"
)
// AutoCodeMssql MSSQL适配器
type AutoCodeMssql struct {
db *gorm.DB
dbList map[string]*gorm.DB
}
// NewAutoCodeMssql 创建MSSQL适配器
func NewAutoCodeMssql(db *gorm.DB, dbList map[string]*gorm.DB) *AutoCodeMssql {
return &AutoCodeMssql{
db: db,
dbList: dbList,
}
}
// GetDB 获取数据库的所有数据库名
func (s *AutoCodeMssql) GetDB(ctx context.Context) ([]Db, error) {
var entities []Db
sql := "select name AS 'database' from sys.databases;"
err := s.db.WithContext(ctx).Raw(sql).Scan(&entities).Error
return entities, err
}
// GetTables 获取数据库的所有表名
func (s *AutoCodeMssql) GetTables(ctx context.Context, dbName string) ([]Table, error) {
var entities []Table
sql := fmt.Sprintf(`select name as 'table_name' from %s.DBO.sysobjects where xtype='U'`, dbName)
err := s.db.WithContext(ctx).Raw(sql).Scan(&entities).Error
return entities, err
}
// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等
func (s *AutoCodeMssql) GetColumn(ctx context.Context, tableName, dbName string) ([]Column, error) {
var entities []Column
sql := fmt.Sprintf(`
SELECT
sc.name AS column_name,
st.name AS data_type,
sc.max_length AS data_type_long,
CASE
WHEN pk.object_id IS NOT NULL THEN 1
ELSE 0
END AS primary_key,
sc.column_id
FROM
%s.sys.columns sc
JOIN
sys.types st ON sc.user_type_id=st.user_type_id
LEFT JOIN
%s.sys.objects so ON so.name='%s' AND so.type='U'
LEFT JOIN
%s.sys.indexes si ON si.object_id = so.object_id AND si.is_primary_key = 1
LEFT JOIN
%s.sys.index_columns sic ON sic.object_id = si.object_id AND sic.index_id = si.index_id AND sic.column_id = sc.column_id
LEFT JOIN
%s.sys.key_constraints pk ON pk.object_id = si.object_id
WHERE
st.is_user_defined=0 AND sc.object_id = so.object_id
ORDER BY
sc.column_id
`, dbName, dbName, tableName, dbName, dbName, dbName)
err := s.db.WithContext(ctx).Raw(sql).Scan(&entities).Error
return entities, err
}
+74
View File
@@ -0,0 +1,74 @@
package system
import (
"context"
"gorm.io/gorm"
)
// AutoCodeMysql MySQL适配器
type AutoCodeMysql struct {
db *gorm.DB
dbList map[string]*gorm.DB
}
// NewAutoCodeMysql 创建MySQL适配器
func NewAutoCodeMysql(db *gorm.DB, dbList map[string]*gorm.DB) *AutoCodeMysql {
return &AutoCodeMysql{
db: db,
dbList: dbList,
}
}
// GetDB 获取数据库的所有数据库名
func (s *AutoCodeMysql) GetDB(ctx context.Context) ([]Db, error) {
var entities []Db
sql := "SELECT SCHEMA_NAME AS `database` FROM INFORMATION_SCHEMA.SCHEMATA;"
err := s.db.WithContext(ctx).Raw(sql).Scan(&entities).Error
return entities, err
}
// GetTables 获取数据库的所有表名
func (s *AutoCodeMysql) GetTables(ctx context.Context, dbName string) ([]Table, error) {
var entities []Table
sql := `select table_name as table_name from information_schema.tables where table_schema = ?`
err := s.db.WithContext(ctx).Raw(sql, dbName).Scan(&entities).Error
return entities, err
}
// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等
func (s *AutoCodeMysql) GetColumn(ctx context.Context, tableName, dbName string) ([]Column, error) {
var entities []Column
sql := `
SELECT
c.COLUMN_NAME column_name,
c.DATA_TYPE data_type,
CASE c.DATA_TYPE
WHEN 'longtext' THEN c.CHARACTER_MAXIMUM_LENGTH
WHEN 'varchar' THEN c.CHARACTER_MAXIMUM_LENGTH
WHEN 'double' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE)
WHEN 'decimal' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE)
WHEN 'int' THEN c.NUMERIC_PRECISION
WHEN 'bigint' THEN c.NUMERIC_PRECISION
ELSE ''
END AS data_type_long,
c.COLUMN_COMMENT column_comment,
CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS primary_key,
c.ORDINAL_POSITION
FROM
INFORMATION_SCHEMA.COLUMNS c
LEFT JOIN
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
ON
c.TABLE_SCHEMA = kcu.TABLE_SCHEMA
AND c.TABLE_NAME = kcu.TABLE_NAME
AND c.COLUMN_NAME = kcu.COLUMN_NAME
AND kcu.CONSTRAINT_NAME = 'PRIMARY'
WHERE
c.TABLE_NAME = ?
AND c.TABLE_SCHEMA = ?
ORDER BY
c.ORDINAL_POSITION;`
err := s.db.WithContext(ctx).Raw(sql, tableName, dbName).Scan(&entities).Error
return entities, err
}
+75
View File
@@ -0,0 +1,75 @@
package system
import (
"context"
"gorm.io/gorm"
)
// AutoCodeOracle Oracle适配器
type AutoCodeOracle struct {
db *gorm.DB
dbList map[string]*gorm.DB
}
// NewAutoCodeOracle 创建Oracle适配器
func NewAutoCodeOracle(db *gorm.DB, dbList map[string]*gorm.DB) *AutoCodeOracle {
return &AutoCodeOracle{
db: db,
dbList: dbList,
}
}
// GetDB 获取数据库的所有数据库名
func (s *AutoCodeOracle) GetDB(ctx context.Context) ([]Db, error) {
var entities []Db
sql := `SELECT lower(username) AS "database" FROM all_users`
err := s.db.WithContext(ctx).Raw(sql).Scan(&entities).Error
return entities, err
}
// GetTables 获取数据库的所有表名
func (s *AutoCodeOracle) GetTables(ctx context.Context, dbName string) ([]Table, error) {
var entities []Table
sql := `select lower(table_name) as "table_name" from all_tables where lower(owner) = ?`
err := s.db.WithContext(ctx).Raw(sql, dbName).Scan(&entities).Error
return entities, err
}
// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等
func (s *AutoCodeOracle) GetColumn(ctx context.Context, tableName, dbName string) ([]Column, error) {
var entities []Column
sql := `
SELECT
lower(a.COLUMN_NAME) as "column_name",
(CASE WHEN a.DATA_TYPE = 'NUMBER' AND a.DATA_SCALE=0 THEN 'int' else lower(a.DATA_TYPE) end) as "data_type",
(CASE WHEN a.DATA_TYPE = 'NUMBER' THEN a.DATA_PRECISION else a.DATA_LENGTH end) as "data_type_long",
b.COMMENTS as "column_comment",
(CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END) as "primary_key",
a.COLUMN_ID
FROM
all_tab_columns a
JOIN
all_col_comments b ON a.OWNER = b.OWNER AND a.TABLE_NAME = b.TABLE_NAME AND a.COLUMN_NAME = b.COLUMN_NAME
LEFT JOIN
(
SELECT
acc.OWNER,
acc.TABLE_NAME,
acc.COLUMN_NAME
FROM
all_cons_columns acc
JOIN
all_constraints ac ON acc.OWNER = ac.OWNER AND acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME
WHERE
ac.CONSTRAINT_TYPE = 'P'
) pk ON a.OWNER = pk.OWNER AND a.TABLE_NAME = pk.TABLE_NAME AND a.COLUMN_NAME = pk.COLUMN_NAME
WHERE
lower(a.table_name) = ?
AND lower(a.OWNER) = ?
ORDER BY
a.COLUMN_ID
`
err := s.db.WithContext(ctx).Raw(sql, tableName, dbName).Scan(&entities).Error
return entities, err
}
+59
View File
@@ -14,6 +14,43 @@ type SysAutoCodePackage struct {
Module string
}
// TemplateDirMapping 模板目录映射配置
// 用于定义模板目录到输出目录的映射关系
type TemplateDirMapping struct {
// TemplateDir 模板目录名(相对于 resource/{template}/server 或 resource/{template}/web)
TemplateDir string
// TargetDir 目标目录(相对于项目根目录)
TargetDir string
// Description 描述
Description string
}
// KratosDDDServerMappings Kratos DDD 架构服务端目录映射
// 定义模板目录到输出目录的映射关系
// 注意:实际输出路径会在 {TargetDir}/{package}/ 下,文件名根据模板类型确定
var KratosDDDServerMappings = []TemplateDirMapping{
{TemplateDir: "biz", TargetDir: "internal/biz/{package}", Description: "Biz 层: Usecase + Repository 接口"},
{TemplateDir: "data", TargetDir: "internal/data/{package}", Description: "Data 层: Repository 实现"},
{TemplateDir: "handler", TargetDir: "internal/server/handler/{package}", Description: "Handler 层: HTTP Handler"},
{TemplateDir: "api", TargetDir: "internal/server/handler/{package}", Description: "API 层 (兼容旧模板)"},
{TemplateDir: "service", TargetDir: "internal/service/{package}", Description: "Service 层: 服务实现"},
{TemplateDir: "router", TargetDir: "internal/server/router/{package}", Description: "Router 层: 路由配置"},
{TemplateDir: "types", TargetDir: "internal/service/types/{package}/request", Description: "Types 层: 请求/响应类型"},
{TemplateDir: "model", TargetDir: "internal/data/model/{package}", Description: "Model 层: GORM Gen 模型"},
{TemplateDir: "model/request", TargetDir: "internal/service/types/{package}/request", Description: "Model Request 层: 请求类型 (兼容旧模板)"},
}
// KratosDDDWebMappings Kratos DDD 架构前端目录映射
// 定义模板目录到输出目录的映射关系
// 注意:pages 输出到 {TargetDir}/{package}/{entity}/,services 输出到 {TargetDir}/
var KratosDDDWebMappings = []TemplateDirMapping{
{TemplateDir: "pages", TargetDir: "web/src/pages/{package}/{entity}", Description: "React 页面组件"},
{TemplateDir: "services", TargetDir: "web/src/services/kratos", Description: "API 服务"},
{TemplateDir: "types", TargetDir: "web/src/types/{package}", Description: "TypeScript 类型定义"},
{TemplateDir: "api", TargetDir: "web/src/services/kratos", Description: "API 服务 (兼容旧模板)"},
{TemplateDir: "view", TargetDir: "web/src/pages/{package}/{entity}", Description: "Vue 页面 (兼容旧模板)"},
}
// AutoCodePackageRepo 代码包仓储接口
type AutoCodePackageRepo interface {
Create(ctx context.Context, pkg *SysAutoCodePackage) error
@@ -24,6 +61,7 @@ type AutoCodePackageRepo interface {
FindByPackageName(ctx context.Context, packageName string) (*SysAutoCodePackage, error)
BatchCreate(ctx context.Context, pkgs []SysAutoCodePackage) error
DeleteByIDs(ctx context.Context, ids []uint) error
Templates(ctx context.Context, entity *SysAutoCodePackage, info interface{}, isPackage bool) (map[string]string, map[string]Ast, map[string]string, error)
}
// AutoCodePackageUsecase 代码包用例
@@ -75,3 +113,24 @@ func (uc *AutoCodePackageUsecase) BatchCreate(ctx context.Context, pkgs []SysAut
func (uc *AutoCodePackageUsecase) DeleteByIDs(ctx context.Context, ids []uint) error {
return uc.repo.DeleteByIDs(ctx, ids)
}
// Templates 获取模板
func (uc *AutoCodePackageUsecase) Templates(ctx context.Context, entity *SysAutoCodePackage, info interface{}, isPackage bool) (map[string]string, map[string]Ast, map[string]string, error) {
return uc.repo.Templates(ctx, entity, info, isPackage)
}
// GetTemplateNames 获取所有模版文件夹名称
func (uc *AutoCodePackageUsecase) GetTemplateNames(ctx context.Context) ([]string, error) {
// 返回固定的模版列表
return []string{"package", "plugin"}, nil
}
// GetServerMappings 获取服务端目录映射
func (uc *AutoCodePackageUsecase) GetServerMappings() []TemplateDirMapping {
return KratosDDDServerMappings
}
// GetWebMappings 获取前端目录映射
func (uc *AutoCodePackageUsecase) GetWebMappings() []TemplateDirMapping {
return KratosDDDWebMappings
}
+121
View File
@@ -0,0 +1,121 @@
package system
import (
"context"
"gorm.io/gorm"
)
// AutoCodePgsql PostgreSQL适配器
type AutoCodePgsql struct {
db *gorm.DB
dbList map[string]*gorm.DB
}
// NewAutoCodePgsql 创建PostgreSQL适配器
func NewAutoCodePgsql(db *gorm.DB, dbList map[string]*gorm.DB) *AutoCodePgsql {
return &AutoCodePgsql{
db: db,
dbList: dbList,
}
}
// GetDB 获取数据库的所有数据库名
func (a *AutoCodePgsql) GetDB(ctx context.Context) ([]Db, error) {
var entities []Db
sql := `SELECT datname as database FROM pg_database WHERE datistemplate = false`
err := a.db.WithContext(ctx).Raw(sql).Scan(&entities).Error
return entities, err
}
// GetTables 获取数据库的所有表名
func (a *AutoCodePgsql) GetTables(ctx context.Context, dbName string) ([]Table, error) {
var entities []Table
sql := `select table_name as table_name from information_schema.tables where table_catalog = ? and table_schema = ?`
err := a.db.WithContext(ctx).Raw(sql, dbName, "public").Scan(&entities).Error
return entities, err
}
// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等
func (a *AutoCodePgsql) GetColumn(ctx context.Context, tableName, dbName string) ([]Column, error) {
sql := `
SELECT
psc.COLUMN_NAME AS COLUMN_NAME,
psc.udt_name AS data_type,
CASE
psc.udt_name
WHEN 'text' THEN
concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH )
WHEN 'varchar' THEN
concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH )
WHEN 'smallint' THEN
concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE )
WHEN 'decimal' THEN
concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE )
WHEN 'integer' THEN
concat_ws ( '', '', psc.NUMERIC_PRECISION )
WHEN 'int4' THEN
concat_ws ( '', '', psc.NUMERIC_PRECISION )
WHEN 'int8' THEN
concat_ws ( '', '', psc.NUMERIC_PRECISION )
WHEN 'bigint' THEN
concat_ws ( '', '', psc.NUMERIC_PRECISION )
WHEN 'timestamp' THEN
concat_ws ( '', '', psc.datetime_precision )
ELSE ''
END AS data_type_long,
(
SELECT
pd.description
FROM
pg_description pd
WHERE
(pd.objoid,pd.objsubid) in (
SELECT pa.attrelid,pa.attnum
FROM
pg_attribute pa
WHERE pa.attrelid = ( SELECT oid FROM pg_class pc WHERE
pc.relname = psc.table_name
)
and attname = psc.column_name
)
) AS column_comment,
(
SELECT
COUNT(*)
FROM
pg_constraint
WHERE
contype = 'p'
AND conrelid = (
SELECT
oid
FROM
pg_class
WHERE
relname = psc.table_name
)
AND conkey::int[] @> ARRAY[(
SELECT
attnum::integer
FROM
pg_attribute
WHERE
attrelid = conrelid
AND attname = psc.column_name
)]
) > 0 AS primary_key,
psc.ordinal_position
FROM
INFORMATION_SCHEMA.COLUMNS psc
WHERE
table_catalog = ?
AND table_schema = 'public'
AND TABLE_NAME = ?
ORDER BY
psc.ordinal_position;
`
var entities []Column
err := a.db.WithContext(ctx).Raw(sql, dbName, tableName).Scan(&entities).Error
return entities, err
}
+353
View File
@@ -0,0 +1,353 @@
package system
import (
"bytes"
"context"
"go/parser"
"go/printer"
"go/token"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"github.com/go-kratos/kratos/v2/log"
"github.com/mholt/archives"
cp "github.com/otiai10/copy"
"github.com/pkg/errors"
"kra/internal/conf"
"kra/pkg/utils"
)
// AutoCodePluginRepo 插件仓储接口
type AutoCodePluginRepo interface {
GetMenusByIDs(ctx context.Context, ids []uint) ([]SysBaseMenu, error)
GetApisByIDs(ctx context.Context, ids []uint) ([]SysApi, error)
}
// AutoCodePluginUsecase 插件用例
type AutoCodePluginUsecase struct {
repo AutoCodePluginRepo
config *conf.AutoCodeConfig
log *log.Helper
}
// NewAutoCodePluginUsecase 创建插件用例
func NewAutoCodePluginUsecase(
repo AutoCodePluginRepo,
config *conf.AutoCodeConfig,
logger log.Logger,
) *AutoCodePluginUsecase {
return &AutoCodePluginUsecase{
repo: repo,
config: config,
log: log.NewHelper(logger),
}
}
// Install 插件安装
func (uc *AutoCodePluginUsecase) Install(file *multipart.FileHeader) (web, server int, err error) {
const PLUGINPATH = "./kra-plug-temp/"
defer os.RemoveAll(PLUGINPATH)
if _, err = os.Stat(PLUGINPATH); os.IsNotExist(err) {
os.Mkdir(PLUGINPATH, os.ModePerm)
}
src, err := file.Open()
if err != nil {
return -1, -1, err
}
defer src.Close()
out, err := os.Create(PLUGINPATH + file.Filename)
if err != nil {
return -1, -1, err
}
defer out.Close()
_, err = io.Copy(out, src)
if err != nil {
return -1, -1, err
}
paths, err := utils.Unzip(PLUGINPATH+file.Filename, PLUGINPATH)
if err != nil {
return -1, -1, err
}
paths = uc.filterFile(paths)
var webIndex = -1
var serverIndex = -1
webPlugin := ""
serverPlugin := ""
for i := range paths {
paths[i] = filepath.ToSlash(paths[i])
pathArr := strings.Split(paths[i], "/")
ln := len(pathArr)
if ln < 4 {
continue
}
if pathArr[2]+"/"+pathArr[3] == `server/plugin` && len(serverPlugin) == 0 {
serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
}
if pathArr[2]+"/"+pathArr[3] == `web/plugin` && len(webPlugin) == 0 {
webPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
}
}
if len(serverPlugin) == 0 && len(webPlugin) == 0 {
uc.log.Error("非标准插件,请按照文档自动迁移使用")
return webIndex, serverIndex, errors.New("非标准插件,请按照文档自动迁移使用")
}
if len(serverPlugin) != 0 {
err = uc.installation(serverPlugin, uc.config.Server, uc.config.Server)
if err != nil {
return webIndex, serverIndex, err
}
}
if len(webPlugin) != 0 {
err = uc.installation(webPlugin, uc.config.Server, uc.config.Web)
if err != nil {
return webIndex, serverIndex, err
}
}
return 1, 1, err
}
// installation 安装插件
func (uc *AutoCodePluginUsecase) installation(path string, formPath string, toPath string) error {
arr := strings.Split(filepath.ToSlash(path), "/")
ln := len(arr)
if ln < 3 {
return errors.New("路径格式错误")
}
name := arr[ln-3]
form := filepath.Join(uc.config.Root, formPath, path)
to := filepath.Join(uc.config.Root, toPath, "plugin")
if _, err := os.Stat(to + name); err == nil {
uc.log.Error("已存在同名插件,请自行手动安装", "to", to)
return errors.New(toPath + "已存在同名插件,请自行手动安装")
}
return cp.Copy(form, to, cp.Options{Skip: uc.skipMacSpecialDocument})
}
// filterFile 过滤文件
func (uc *AutoCodePluginUsecase) filterFile(paths []string) []string {
np := make([]string, 0, len(paths))
for _, path := range paths {
if ok, _ := uc.skipMacSpecialDocument(nil, path, ""); ok {
continue
}
np = append(np, path)
}
return np
}
// skipMacSpecialDocument 跳过Mac特殊文件
func (uc *AutoCodePluginUsecase) skipMacSpecialDocument(_ os.FileInfo, src, _ string) (bool, error) {
if strings.Contains(src, ".DS_Store") || strings.Contains(src, "__MACOSX") {
return true, nil
}
return false, nil
}
// PubPlug 发布插件
func (uc *AutoCodePluginUsecase) PubPlug(plugName string) (zipPath string, err error) {
if plugName == "" {
return "", errors.New("插件名称不能为空")
}
// 防止路径穿越
plugName = filepath.Clean(plugName)
webPath := filepath.Join(uc.config.Root, uc.config.Web, "plugin", plugName)
serverPath := filepath.Join(uc.config.Root, uc.config.Server, "plugin", plugName)
// 判断目录是否存在
if _, err = os.Stat(webPath); err != nil {
return "", errors.New("web路径不存在")
}
if _, err = os.Stat(serverPath); err != nil {
return "", errors.New("server路径不存在")
}
fileName := plugName + ".zip"
// 创建zip文件
files, err := archives.FilesFromDisk(context.Background(), nil, map[string]string{
webPath: plugName + "/web/plugin/" + plugName,
serverPath: plugName + "/server/plugin/" + plugName,
})
if err != nil {
return "", err
}
out, err := os.Create(fileName)
if err != nil {
return "", err
}
defer out.Close()
format := archives.CompressedArchive{
Archival: archives.Zip{},
}
if err = format.Archive(context.Background(), out, files); err != nil {
return "", err
}
return filepath.Join(uc.config.Root, uc.config.Server, fileName), nil
}
// InitMenuRequest 初始化菜单请求
type InitMenuRequest struct {
PlugName string `json:"plugName"`
ParentMenu string `json:"parentMenu"`
Menus []uint `json:"menus"`
}
// InitMenu 初始化菜单
func (uc *AutoCodePluginUsecase) InitMenu(ctx context.Context, menuInfo InitMenuRequest) error {
menuPath := filepath.Join(uc.config.Root, uc.config.Server, "plugin", menuInfo.PlugName, "initialize", "menu.go")
src, err := os.ReadFile(menuPath)
if err != nil {
return err
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
if err != nil {
return err
}
arrayAst := utils.FindArray(astFile, "model", "SysBaseMenu")
if arrayAst == nil {
return errors.New("未找到菜单数组")
}
// 查询菜单
menus, err := uc.repo.GetMenusByIDs(ctx, menuInfo.Menus)
if err != nil {
return err
}
// 添加父菜单
parentMenu := SysBaseMenu{
ParentId: 0,
Path: menuInfo.PlugName + "Menu",
Name: menuInfo.PlugName + "Menu",
Hidden: false,
Component: "view/routerHolder.vue",
Sort: 0,
Meta: Meta{
Title: menuInfo.ParentMenu,
Icon: "school",
},
}
allMenus := append([]SysBaseMenu{parentMenu}, menus...)
// 转换为utils.SysBaseMenu类型
utilsMenus := make([]utils.SysBaseMenu, len(allMenus))
for i, m := range allMenus {
utilsMenus[i] = utils.SysBaseMenu{
ParentId: m.ParentId,
Path: m.Path,
Name: m.Name,
Hidden: m.Hidden,
Component: m.Component,
Sort: m.Sort,
Title: m.Meta.Title,
Icon: m.Meta.Icon,
}
// 转换参数
for _, p := range m.Parameters {
utilsMenus[i].Parameters = append(utilsMenus[i].Parameters, utils.SysBaseMenuParameter{
Type: p.Type,
Key: p.Key,
Value: p.Value,
})
}
// 转换按钮
for _, b := range m.MenuBtn {
utilsMenus[i].MenuBtn = append(utilsMenus[i].MenuBtn, utils.SysBaseMenuBtn{
Name: b.Name,
Desc: b.Desc,
})
}
}
menuExpr := utils.CreateMenuStructAst(utilsMenus)
arrayAst.Elts = *menuExpr
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
return os.WriteFile(menuPath, bf.Bytes(), 0666)
}
// InitApiRequest 初始化API请求
type InitApiRequest struct {
PlugName string `json:"plugName"`
APIs []uint `json:"apis"`
}
// InitAPI 初始化API
func (uc *AutoCodePluginUsecase) InitAPI(ctx context.Context, apiInfo InitApiRequest) error {
apiPath := filepath.Join(uc.config.Root, uc.config.Server, "plugin", apiInfo.PlugName, "initialize", "api.go")
src, err := os.ReadFile(apiPath)
if err != nil {
return err
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
if err != nil {
return err
}
arrayAst := utils.FindArray(astFile, "model", "SysApi")
if arrayAst == nil {
return errors.New("未找到API数组")
}
// 查询API
apis, err := uc.repo.GetApisByIDs(ctx, apiInfo.APIs)
if err != nil {
return err
}
// 转换为utils.SysApi类型
utilsApis := make([]utils.SysApi, len(apis))
for i, a := range apis {
utilsApis[i] = utils.SysApi{
Path: a.Path,
Description: a.Description,
ApiGroup: a.ApiGroup,
Method: a.Method,
}
}
apisExpr := utils.CreateApiStructAst(utilsApis)
arrayAst.Elts = *apisExpr
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
return os.WriteFile(apiPath, bf.Bytes(), 0666)
}
+88
View File
@@ -0,0 +1,88 @@
package system
import (
"context"
"fmt"
"path/filepath"
"strings"
"gorm.io/gorm"
)
// AutoCodeSqlite SQLite适配器
type AutoCodeSqlite struct {
db *gorm.DB
dbList map[string]*gorm.DB
}
// NewAutoCodeSqlite 创建SQLite适配器
func NewAutoCodeSqlite(db *gorm.DB, dbList map[string]*gorm.DB) *AutoCodeSqlite {
return &AutoCodeSqlite{
db: db,
dbList: dbList,
}
}
// GetDB 获取数据库的所有数据库名
func (s *AutoCodeSqlite) GetDB(ctx context.Context) ([]Db, error) {
var entities []Db
sql := "PRAGMA database_list;"
var databaseList []struct {
File string `gorm:"column:file"`
}
err := s.db.WithContext(ctx).Raw(sql).Find(&databaseList).Error
if err != nil {
return nil, err
}
for _, database := range databaseList {
if database.File != "" {
fileName := filepath.Base(database.File)
fileExt := filepath.Ext(fileName)
fileNameWithoutExt := strings.TrimSuffix(fileName, fileExt)
entities = append(entities, Db{Database: fileNameWithoutExt})
}
}
return entities, nil
}
// GetTables 获取数据库的所有表名
func (s *AutoCodeSqlite) GetTables(ctx context.Context, dbName string) ([]Table, error) {
var entities []Table
sql := `SELECT name FROM sqlite_master WHERE type='table'`
var tableNames []string
err := s.db.WithContext(ctx).Raw(sql).Find(&tableNames).Error
if err != nil {
return nil, err
}
for _, tableName := range tableNames {
entities = append(entities, Table{TableName: tableName})
}
return entities, nil
}
// GetColumn 获取指定数据表的所有字段名,类型值等
func (s *AutoCodeSqlite) GetColumn(ctx context.Context, tableName, dbName string) ([]Column, error) {
var entities []Column
sql := fmt.Sprintf("PRAGMA table_info(%s);", tableName)
var columnInfos []struct {
Name string `gorm:"column:name"`
Type string `gorm:"column:type"`
Pk int `gorm:"column:pk"`
}
err := s.db.WithContext(ctx).Raw(sql).Scan(&columnInfos).Error
if err != nil {
return nil, err
}
for _, columnInfo := range columnInfos {
pk := 0
if columnInfo.Pk == 1 {
pk = 1
}
entities = append(entities, Column{
ColumnName: columnInfo.Name,
DataType: columnInfo.Type,
PrimaryKey: pk,
})
}
return entities, nil
}
+705
View File
@@ -0,0 +1,705 @@
package system
import (
"context"
"encoding/json"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/go-kratos/kratos/v2/log"
"github.com/pkg/errors"
"gorm.io/gorm"
"kra/internal/conf"
"kra/pkg/utils"
"kra/pkg/utils/autocode"
)
// AutoCodeTemplateRepo 自动代码模板仓储接口
type AutoCodeTemplateRepo interface {
GetPackageByName(ctx context.Context, packageName string) (*SysAutoCodePackage, error)
CreateApis(ctx context.Context, apis []SysApi) ([]uint, error)
GetMenuByName(ctx context.Context, name string) (*SysBaseMenu, error)
CreateMenu(ctx context.Context, menu *SysBaseMenu) error
CreateExportTemplate(ctx context.Context, tpl *SysExportTemplate) error
Repeat(ctx context.Context, businessDB, structName, abbreviation, pkg string) bool
}
// AutoCodeTemplateUsecase 自动代码模板用例
type AutoCodeTemplateUsecase struct {
repo AutoCodeTemplateRepo
historyRepo AutoCodeHistoryRepo
packageRepo AutoCodePackageRepo
config *conf.AutoCodeConfig
log *log.Helper
db *gorm.DB
}
// NewAutoCodeTemplateUsecase 创建自动代码模板用例
func NewAutoCodeTemplateUsecase(
repo AutoCodeTemplateRepo,
historyRepo AutoCodeHistoryRepo,
packageRepo AutoCodePackageRepo,
config *conf.AutoCodeConfig,
logger log.Logger,
db *gorm.DB,
) *AutoCodeTemplateUsecase {
return &AutoCodeTemplateUsecase{
repo: repo,
historyRepo: historyRepo,
packageRepo: packageRepo,
config: config,
log: log.NewHelper(logger),
db: db,
}
}
// checkPackage 检查包结构是否完整
// 支持 Kratos DDD 层次结构检查:
// - biz/{package}/enter.go - 业务逻辑层入口
// - data/{package}/enter.go - 数据访问层入口
// - service/{package}/enter.go - 服务层入口
// - server/handler/{package}/enter.go - HTTP Handler 层入口
// - server/router/{package}/enter.go - 路由层入口
func (uc *AutoCodeTemplateUsecase) checkPackage(pkg string, template string) error {
switch template {
case "package":
// Kratos DDD 架构目录检查
// 检查 Biz 层
bizEnter := filepath.Join(uc.config.Root, uc.config.Server, "internal", "biz", pkg, "enter.go")
if _, err := os.Stat(bizEnter); err != nil {
return fmt.Errorf("package结构异常,缺少internal/biz/%s/enter.go", pkg)
}
// 检查 Data 层
dataEnter := filepath.Join(uc.config.Root, uc.config.Server, "internal", "data", pkg, "enter.go")
if _, err := os.Stat(dataEnter); err != nil {
return fmt.Errorf("package结构异常,缺少internal/data/%s/enter.go", pkg)
}
// 检查 Service 层
serviceEnter := filepath.Join(uc.config.Root, uc.config.Server, "internal", "service", pkg, "enter.go")
if _, err := os.Stat(serviceEnter); err != nil {
return fmt.Errorf("package结构异常,缺少internal/service/%s/enter.go", pkg)
}
// 检查 Handler 层
handlerEnter := filepath.Join(uc.config.Root, uc.config.Server, "internal", "server", "handler", pkg, "enter.go")
if _, err := os.Stat(handlerEnter); err != nil {
return fmt.Errorf("package结构异常,缺少internal/server/handler/%s/enter.go", pkg)
}
// 检查 Router 层
routerEnter := filepath.Join(uc.config.Root, uc.config.Server, "internal", "server", "router", pkg, "enter.go")
if _, err := os.Stat(routerEnter); err != nil {
return fmt.Errorf("package结构异常,缺少internal/server/router/%s/enter.go", pkg)
}
case "plugin":
pluginEnter := filepath.Join(uc.config.Root, uc.config.Server, "internal", "plugin", pkg, "plugin.go")
if _, err := os.Stat(pluginEnter); err != nil {
return fmt.Errorf("plugin结构异常,缺少internal/plugin/%s/plugin.go", pkg)
}
}
return nil
}
// Create 创建生成自动化代码
func (uc *AutoCodeTemplateUsecase) Create(ctx context.Context, info *AutoCodeInfo) error {
// 查询包信息
autoPkg, err := uc.repo.GetPackageByName(ctx, info.Package)
if err != nil {
return errors.Wrap(err, "查询包失败!")
}
// 检查包结构
if err = uc.checkPackage(info.Package, autoPkg.Template); err != nil {
return err
}
// 检查重复创建
if uc.repo.Repeat(ctx, info.BusinessDB, info.StructName, info.Abbreviation, info.Package) {
return errors.New("已经创建过此数据结构,请勿重复创建!")
}
// 生成代码
generate, templates, injections, err := uc.generate(ctx, info, autoPkg)
if err != nil {
return err
}
// 写入文件
for key, builder := range generate {
if err = os.MkdirAll(filepath.Dir(key), os.ModePerm); err != nil {
return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", key)
}
if err = os.WriteFile(key, []byte(builder.String()), 0666); err != nil {
return errors.Wrapf(err, "[filepath:%s]写入文件失败!", key)
}
}
// 创建历史记录
history := &SysAutoCodeHistory{
Table: info.TableName,
Package: info.Package,
StructName: info.StructName,
Abbreviation: info.Abbreviation,
BusinessDB: info.BusinessDB,
Description: info.Description,
Templates: templates,
}
// 自动创建API
if info.AutoCreateApiToSql && !info.OnlyTemplate {
apis := uc.buildApis(info)
apiIDs, err := uc.repo.CreateApis(ctx, apis)
if err != nil {
return err
}
history.ApiIDs = apiIDs
}
// 自动创建菜单
if info.AutoCreateMenuToSql {
menu, err := uc.repo.GetMenuByName(ctx, info.Abbreviation)
if err == nil && menu != nil {
history.MenuID = menu.ID
} else {
newMenu := uc.buildMenu(info, autoPkg.Template)
if info.AutoCreateBtnAuth && !info.OnlyTemplate {
newMenu.MenuBtn = uc.buildMenuBtns(info)
}
if err = uc.repo.CreateMenu(ctx, newMenu); err != nil {
return errors.Wrap(err, "创建菜单失败!")
}
history.MenuID = newMenu.ID
}
}
// 处理Excel导出模板
if info.HasExcel {
exportTpl := uc.buildExportTemplate(info)
if err = uc.repo.CreateExportTemplate(ctx, exportTpl); err != nil {
return err
}
history.ExportTemplateID = exportTpl.ID
}
// 保存注入信息
history.Injections = make(map[string]string, len(injections))
for key, value := range injections {
bytes, _ := json.Marshal(value)
history.Injections[key] = string(bytes)
}
// 创建历史记录
return uc.historyRepo.Create(ctx, history)
}
// Preview 预览自动化代码
func (uc *AutoCodeTemplateUsecase) Preview(ctx context.Context, info *AutoCodeInfo) (map[string]string, error) {
// 查询包信息
entity, err := uc.repo.GetPackageByName(ctx, info.Package)
if err != nil {
return nil, errors.Wrap(err, "查询包失败!")
}
// 检查重复创建
if uc.repo.Repeat(ctx, info.BusinessDB, info.StructName, info.Abbreviation, info.Package) && !info.IsAdd {
return nil, errors.New("已经创建过此数据结构或重复简称,请勿重复创建!")
}
preview := make(map[string]string)
codes, _, _, err := uc.generate(ctx, info, entity)
if err != nil {
return nil, err
}
for key, writer := range codes {
if len(key) > len(uc.config.Root) {
key, _ = filepath.Rel(uc.config.Root, key)
}
suffix := filepath.Ext(key)[1:]
var builder strings.Builder
builder.WriteString("```" + suffix + "\n\n")
builder.WriteString(writer.String())
builder.WriteString("\n\n```")
preview[key] = builder.String()
}
return preview, nil
}
// generate 生成代码
func (uc *AutoCodeTemplateUsecase) generate(ctx context.Context, info *AutoCodeInfo, entity *SysAutoCodePackage) (map[string]strings.Builder, map[string]string, map[string]interface{}, error) {
dataList, asts, fileList, err := uc.packageRepo.Templates(ctx, entity, info, false)
if err != nil {
return nil, nil, nil, err
}
code := make(map[string]strings.Builder)
for templatePath := range dataList {
// 获取目标文件路径
targetPath, ok := fileList[templatePath]
if !ok {
continue
}
// 解析并执行模板
files, err := template.New(filepath.Base(templatePath)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(templatePath)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", templatePath)
}
var builder strings.Builder
if err = files.Execute(&builder, info); err != nil {
return nil, nil, nil, errors.Wrapf(err, "[filepath:%s]生成文件失败!", targetPath)
}
code[targetPath] = builder
}
injections := make(map[string]interface{}, len(asts))
for key, value := range asts {
keys := strings.Split(key, "=>")
if len(keys) == 2 {
if keys[1] == TypePluginInitializeV2 {
continue
}
if info.OnlyTemplate {
if keys[1] == TypePackageInitializeGorm || keys[1] == TypePluginInitializeGorm {
continue
}
}
if !info.AutoMigrate {
if keys[1] == TypePackageInitializeGorm || keys[1] == TypePluginInitializeGorm {
continue
}
}
var builder strings.Builder
parse, _ := value.Parse("", &builder)
if parse != nil {
_ = value.Injection(parse)
if err = value.Format("", &builder, parse); err != nil {
return nil, nil, nil, err
}
code[keys[0]] = builder
injections[keys[1]] = value
fmt.Println(keys[0], "注入成功!")
}
}
}
return code, fileList, injections, nil
}
// AddFunc 添加函数
func (uc *AutoCodeTemplateUsecase) AddFunc(info *AutoFuncInfo) error {
autoPkg, err := uc.packageRepo.FindByPackageName(context.Background(), info.Package)
if err != nil {
return err
}
if autoPkg.Template != "package" {
info.IsPlugin = true
}
if err = uc.addTemplateToFile("api.go", info); err != nil {
return err
}
if err = uc.addTemplateToFile("server.go", info); err != nil {
return err
}
if err = uc.addTemplateToFile("api.js", info); err != nil {
return err
}
return uc.addTemplateToAst("router", info)
}
// GetApiAndServer 获取API和Server代码
func (uc *AutoCodeTemplateUsecase) GetApiAndServer(info *AutoFuncInfo) (map[string]string, error) {
autoPkg, err := uc.packageRepo.FindByPackageName(context.Background(), info.Package)
if err != nil {
return nil, err
}
if autoPkg.Template != "package" {
info.IsPlugin = true
}
apiStr, err := uc.getTemplateStr("api.go", info)
if err != nil {
return nil, err
}
serverStr, err := uc.getTemplateStr("server.go", info)
if err != nil {
return nil, err
}
jsStr, err := uc.getTemplateStr("api.js", info)
if err != nil {
return nil, err
}
return map[string]string{"api": apiStr, "server": serverStr, "js": jsStr}, nil
}
// getTemplateStr 获取模板字符串
func (uc *AutoCodeTemplateUsecase) getTemplateStr(t string, info *AutoFuncInfo) (string, error) {
tempPath := filepath.Join(uc.config.Root, uc.config.Server, "resource", "function", t+".tpl")
files, err := template.New(filepath.Base(tempPath)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(tempPath)
if err != nil {
return "", errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", tempPath)
}
var builder strings.Builder
if err = files.Execute(&builder, info); err != nil {
return "", errors.Wrapf(err, "[filepath:%s]生成文件失败!", tempPath)
}
return builder.String(), nil
}
// addTemplateToAst 添加模板到AST
// 支持 Kratos DDD 架构的路由注入
func (uc *AutoCodeTemplateUsecase) addTemplateToAst(t string, info *AutoFuncInfo) error {
// Kratos DDD 架构:路由文件在 internal/server/router/{package}/
tPath := filepath.Join(uc.config.Root, uc.config.Server, "internal", "server", "router", info.Package, info.HumpPackageName+".go")
funcName := fmt.Sprintf("Init%sRouter", info.StructName)
routerStr := "RouterWithoutAuth"
if info.IsAuth {
routerStr = "Router"
}
stmtStr := fmt.Sprintf("%s%s.%s(\"%s\", %sApi.%s)", info.Abbreviation, routerStr, info.Method, info.Router, info.Abbreviation, info.FuncName)
if info.IsPlugin {
tPath = filepath.Join(uc.config.Root, uc.config.Server, "internal", "plugin", info.Package, "router", info.HumpPackageName+".go")
stmtStr = fmt.Sprintf("group.%s(\"%s\", api%s.%s)", info.Method, info.Router, info.StructName, info.FuncName)
funcName = "Init"
}
src, err := os.ReadFile(tPath)
if err != nil {
return err
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
if err != nil {
return err
}
funcDecl := utils.FindFunction(astFile, funcName)
stmtNode := utils.CreateStmt(stmtStr)
if info.IsAuth {
for i := 0; i < len(funcDecl.Body.List); i++ {
st := funcDecl.Body.List[i]
if blockStmt, ok := st.(*ast.BlockStmt); ok {
blockStmt.List = append(blockStmt.List, stmtNode)
break
}
}
} else {
for i := len(funcDecl.Body.List) - 1; i >= 0; i-- {
st := funcDecl.Body.List[i]
if blockStmt, ok := st.(*ast.BlockStmt); ok {
blockStmt.List = append(blockStmt.List, stmtNode)
break
}
}
}
f, err := os.Create(tPath)
if err != nil {
return err
}
defer f.Close()
return format.Node(f, fileSet, astFile)
}
// addTemplateToFile 添加模板到文件
// 支持 Kratos DDD 架构的文件路径:
// - handler: internal/server/handler/{package}/
// - service: internal/service/{package}/
// - api.ts: web/src/services/kratos/
func (uc *AutoCodeTemplateUsecase) addTemplateToFile(t string, info *AutoFuncInfo) error {
getTemplateStr, err := uc.getTemplateStr(t, info)
if err != nil {
return err
}
var target string
switch t {
case "api.go":
// Kratos DDD 架构:Handler 在 internal/server/handler/{package}/
if info.IsAi && info.ApiFunc != "" {
getTemplateStr = info.ApiFunc
}
target = filepath.Join(uc.config.Root, uc.config.Server, "internal", "server", "handler", info.Package, info.HumpPackageName+".go")
case "server.go":
// Kratos DDD 架构:Service 在 internal/service/{package}/
if info.IsAi && info.ServerFunc != "" {
getTemplateStr = info.ServerFunc
}
target = filepath.Join(uc.config.Root, uc.config.Server, "internal", "service", info.Package, info.HumpPackageName+".go")
case "api.js":
// Kratos DDD 架构:前端 API 在 web/src/services/kratos/
if info.IsAi && info.JsFunc != "" {
getTemplateStr = info.JsFunc
}
target = filepath.Join(uc.config.Root, uc.config.Web, "src", "services", "kratos", info.PackageName+".ts")
}
if info.IsPlugin {
switch t {
case "api.go":
target = filepath.Join(uc.config.Root, uc.config.Server, "internal", "plugin", info.Package, "api", info.HumpPackageName+".go")
case "server.go":
target = filepath.Join(uc.config.Root, uc.config.Server, "internal", "plugin", info.Package, "service", info.HumpPackageName+".go")
case "api.js":
target = filepath.Join(uc.config.Root, uc.config.Web, "src", "plugin", info.Package, "api", info.PackageName+".ts")
}
}
file, err := os.OpenFile(target, os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return err
}
defer file.Close()
_, err = fmt.Fprintln(file, getTemplateStr)
return err
}
// buildApis 构建API列表
func (uc *AutoCodeTemplateUsecase) buildApis(info *AutoCodeInfo) []SysApi {
apis := []SysApi{
{Path: "/" + info.Abbreviation + "/" + "create" + info.StructName, Description: "新增" + info.Description, ApiGroup: info.Description, Method: "POST"},
{Path: "/" + info.Abbreviation + "/" + "delete" + info.StructName, Description: "删除" + info.Description, ApiGroup: info.Description, Method: "DELETE"},
{Path: "/" + info.Abbreviation + "/" + "delete" + info.StructName + "ByIds", Description: "批量删除" + info.Description, ApiGroup: info.Description, Method: "DELETE"},
{Path: "/" + info.Abbreviation + "/" + "update" + info.StructName, Description: "更新" + info.Description, ApiGroup: info.Description, Method: "PUT"},
{Path: "/" + info.Abbreviation + "/" + "find" + info.StructName, Description: "根据ID获取" + info.Description, ApiGroup: info.Description, Method: "GET"},
{Path: "/" + info.Abbreviation + "/" + "get" + info.StructName + "List", Description: "获取" + info.Description + "列表", ApiGroup: info.Description, Method: "GET"},
}
if info.HasExcel {
apis = append(apis, []SysApi{
{Path: "/" + info.Abbreviation + "/" + "export" + info.StructName + "Excel", Description: "导出" + info.Description + "Excel", ApiGroup: info.Description, Method: "GET"},
{Path: "/" + info.Abbreviation + "/" + "import" + info.StructName + "Excel", Description: "导入" + info.Description + "Excel", ApiGroup: info.Description, Method: "POST"},
{Path: "/" + info.Abbreviation + "/" + "download" + info.StructName + "Template", Description: "下载" + info.Description + "模板", ApiGroup: info.Description, Method: "GET"},
}...)
}
return apis
}
// buildMenu 构建菜单
// 支持 Kratos DDD 架构的前端组件路径:
// - package: pages/{package}/{entity}/index
// - plugin: plugin/{package}/view/{entity}
func (uc *AutoCodeTemplateUsecase) buildMenu(info *AutoCodeInfo, template string) *SysBaseMenu {
// Kratos DDD 架构:React 页面在 pages/{package}/{entity}/
component := "pages/" + info.Package + "/" + info.PackageName + "/index"
if template == "plugin" {
component = "plugin/" + info.Package + "/view/" + info.PackageName
}
return &SysBaseMenu{
ParentId: 0,
Path: info.Abbreviation,
Name: info.Abbreviation,
Hidden: false,
Component: component,
Sort: 0,
Meta: Meta{
Title: info.Description,
Icon: "menu",
},
}
}
// buildMenuBtns 构建菜单按钮
func (uc *AutoCodeTemplateUsecase) buildMenuBtns(info *AutoCodeInfo) []SysBaseMenuBtn {
btns := []SysBaseMenuBtn{
{Name: "add", Desc: "新增"},
{Name: "batchDelete", Desc: "批量删除"},
{Name: "delete", Desc: "删除"},
{Name: "edit", Desc: "编辑"},
{Name: "info", Desc: "详情"},
}
if info.HasExcel {
btns = append(btns, []SysBaseMenuBtn{
{Name: "exportTemplate", Desc: "导出模板"},
{Name: "exportExcel", Desc: "导出Excel"},
{Name: "importExcel", Desc: "导入Excel"},
}...)
}
return btns
}
// buildExportTemplate 构建导出模板
func (uc *AutoCodeTemplateUsecase) buildExportTemplate(info *AutoCodeInfo) *SysExportTemplate {
fieldsMap := make(map[string]string, len(info.Fields))
for _, field := range info.Fields {
if field.Excel {
fieldsMap[field.ColumnName] = field.FieldDesc
}
}
templateInfo, _ := json.Marshal(fieldsMap)
return &SysExportTemplate{
DBName: info.BusinessDB,
Name: info.Package + "_" + info.StructName,
Table: info.TableName,
TemplateID: info.Package + "_" + info.StructName,
TemplateInfo: string(templateInfo),
}
}
// AST类型常量
const (
TypePackageApiEnter = "PackageApiEnter"
TypePackageRouterEnter = "PackageRouterEnter"
TypePackageServiceEnter = "PackageServiceEnter"
TypePackageInitializeGorm = "PackageInitializeGorm"
TypePackageInitializeRouter = "PackageInitializeRouter"
TypePackageGormGen = "PackageGormGen"
TypePluginGen = "PluginGen"
TypePluginApiEnter = "PluginApiEnter"
TypePluginInitializeV2 = "PluginInitializeV2"
TypePluginRouterEnter = "PluginRouterEnter"
TypePluginServiceEnter = "PluginServiceEnter"
TypePluginInitializeGorm = "PluginInitializeGorm"
TypePluginInitializeRouter = "PluginInitializeRouter"
)
// AutoCodeInfo 相关实体类型
type AutoCodeInfo struct {
Package string
PackageT string
TableName string
BusinessDB string
StructName string
PackageName string
Description string
Abbreviation string
HumpPackageName string
GvaModel bool
AutoMigrate bool
AutoCreateResource bool
AutoCreateApiToSql bool
AutoCreateMenuToSql bool
AutoCreateBtnAuth bool
OnlyTemplate bool
IsTree bool
TreeJson string
IsAdd bool
Fields []*AutoCodeField
GenerateWeb bool
GenerateServer bool
Module string
DictTypes []string
PrimaryField *AutoCodeField
DataSourceMap map[string]*DataSource
HasPic bool
HasFile bool
HasTimer bool
NeedSort bool
NeedJSON bool
HasRichText bool
HasDataSource bool
HasSearchTimer bool
HasArray bool
HasExcel bool
CustomMethods []*CustomMethod // 自定义查询方法
HasCustomMethods bool // 是否有自定义查询方法
}
// CustomMethod 自定义查询方法
// 用于在 GORM Gen 生成的代码中添加自定义查询方法
type CustomMethod struct {
Name string `json:"name"` // 方法名称 (如: FindByName, GetActiveUsers)
Description string `json:"description"` // 方法描述
ReturnType string `json:"returnType"` // 返回类型: single(单个), list(列表), count(计数), exists(存在检查)
Params []*CustomMethodParam `json:"params"` // 方法参数
Conditions []*CustomCondition `json:"conditions"` // 查询条件
OrderBy string `json:"orderBy"` // 排序字段 (如: created_at DESC)
Limit int `json:"limit"` // 限制数量 (0表示不限制)
}
// CustomMethodParam 自定义方法参数
type CustomMethodParam struct {
Name string `json:"name"` // 参数名称
Type string `json:"type"` // 参数类型 (string, int, uint, bool, time.Time等)
FieldName string `json:"fieldName"` // 对应的字段名 (用于生成查询条件)
}
// CustomCondition 自定义查询条件
type CustomCondition struct {
FieldName string `json:"fieldName"` // 字段名
Operator string `json:"operator"` // 操作符: eq, neq, gt, gte, lt, lte, like, in, between, isNull, isNotNull
ParamName string `json:"paramName"` // 参数名 (对应 CustomMethodParam.Name)
}
// DataSource 数据源
type DataSource struct {
DBName string `json:"dbName"`
Table string `json:"table"`
Label string `json:"label"`
Value string `json:"value"`
Association int `json:"association"`
HasDeletedAt bool `json:"hasDeletedAt"`
}
// AutoCodeField 自动代码字段
type AutoCodeField struct {
FieldName string `json:"fieldName"`
FieldDesc string `json:"fieldDesc"`
FieldType string `json:"fieldType"`
FieldJson string `json:"fieldJson"`
DataTypeLong string `json:"dataTypeLong"`
Comment string `json:"comment"`
ColumnName string `json:"columnName"`
FieldSearchType string `json:"fieldSearchType"`
FieldSearchHide bool `json:"fieldSearchHide"`
DictType string `json:"dictType"`
Form bool `json:"form"`
Table bool `json:"table"`
Desc bool `json:"desc"`
Excel bool `json:"excel"`
Require bool `json:"require"`
DefaultValue string `json:"defaultValue"`
ErrorText string `json:"errorText"`
Clearable bool `json:"clearable"`
Sort bool `json:"sort"`
PrimaryKey bool `json:"primaryKey"`
DataSource *DataSource `json:"dataSource"`
CheckDataSource bool `json:"checkDataSource"`
FieldIndexType string `json:"fieldIndexType"`
}
// AutoFuncInfo 自动函数信息
type AutoFuncInfo struct {
Package string
FuncName string
Router string
FuncDesc string
BusinessDB string
StructName string
PackageName string
Description string
Abbreviation string
HumpPackageName string
Method string
IsPlugin bool
IsAuth bool
IsPreview bool
IsAi bool
ApiFunc string
ServerFunc string
JsFunc string
}
// SysExportTemplate 导出模板
type SysExportTemplate struct {
ID uint `json:"ID" gorm:"primarykey"`
DBName string `json:"dbName" gorm:"column:db_name;comment:数据库名"`
Name string `json:"name" gorm:"column:name;comment:模板名称"`
Table string `json:"tableName" gorm:"column:table_name;comment:表名"`
TemplateID string `json:"templateId" gorm:"column:template_id;comment:模板ID"`
TemplateInfo string `json:"templateInfo" gorm:"column:template_info;type:text;comment:模板信息"`
}
func (SysExportTemplate) TableName() string {
return "sys_export_templates"
}
@@ -0,0 +1,881 @@
package system
import (
"bytes"
"go/format"
"strings"
"testing"
"text/template"
"kra/pkg/utils/autocode"
)
// TestTemplateFuncMap 测试模板函数映射是否正确注册
func TestTemplateFuncMap(t *testing.T) {
funcMap := autocode.GetTemplateFuncMap()
// 验证必要的函数已注册
requiredFuncs := []string{
"title",
"toPascalCase",
"GenerateField",
"GenerateModelField",
"GenerateSearchField",
"GenerateSearchConditions",
"GenerateGormGenSearchConditions",
"GenerateSearchFormItem",
"GenerateTableColumn",
"GenerateFormItem",
"GenerateDescriptionItem",
"GenerateDefaultFormValue",
"GenerateTSType",
"GenerateReactSearchFormItem",
"GenerateReactTableColumn",
"GenerateReactProTableColumn",
"GenerateReactFormItem",
"GenerateReactDescriptionItem",
"GenerateReactDefaultValue",
"GenerateCustomMethodInterface",
"GenerateCustomMethodImpl",
}
for _, funcName := range requiredFuncs {
if _, ok := funcMap[funcName]; !ok {
t.Errorf("模板函数 %s 未注册", funcName)
}
}
}
// TestGenerateTSType 测试 Go 类型到 TypeScript 类型的映射
func TestGenerateTSType(t *testing.T) {
testCases := []struct {
goType string
expected string
}{
// 整数类型
{"int", "number"},
{"int8", "number"},
{"int16", "number"},
{"int32", "number"},
{"int64", "number"},
// 无符号整数类型
{"uint", "number"},
{"uint8", "number"},
{"uint16", "number"},
{"uint32", "number"},
{"uint64", "number"},
// 浮点类型
{"float32", "number"},
{"float64", "number"},
// 基础类型
{"string", "string"},
{"bool", "boolean"},
// 时间类型
{"time.Time", "string"},
// JSON 类型
{"datatypes.JSON", "Record<string, any>"},
{"json", "Record<string, any>"},
// 媒体类型
{"picture", "string"},
{"pictures", "string[]"},
{"video", "string"},
{"file", "Array<{ name: string; url: string; uid: string }>"},
// 其他类型
{"array", "any[]"},
{"richtext", "string"},
{"enum", "string"},
// 指针类型
{"*string", "string"},
{"*int", "number"},
{"*bool", "boolean"},
{"*time.Time", "string"},
{"*int64", "number"},
{"*float64", "number"},
// 未知类型
{"unknown", "any"},
{"customType", "any"},
{"", "any"},
}
for _, tc := range testCases {
t.Run(tc.goType, func(t *testing.T) {
result := autocode.GenerateTSType(tc.goType)
if result != tc.expected {
t.Errorf("GenerateTSType(%s) = %s, 期望 %s", tc.goType, result, tc.expected)
}
})
}
}
// TestGenerateTSTypePointerRecursion 测试指针类型的递归处理
func TestGenerateTSTypePointerRecursion(t *testing.T) {
testCases := []struct {
name string
goType string
expected string
}{
{"单层指针-string", "*string", "string"},
{"单层指针-int", "*int", "number"},
{"单层指针-bool", "*bool", "boolean"},
{"单层指针-time", "*time.Time", "string"},
{"单层指针-json", "*datatypes.JSON", "Record<string, any>"},
{"单层指针-未知类型", "*unknownType", "any"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateTSType(tc.goType)
if result != tc.expected {
t.Errorf("GenerateTSType(%s) = %s, 期望 %s", tc.goType, result, tc.expected)
}
})
}
}
// TestGenerateTSTypeAllNumericTypes 测试所有数值类型映射
func TestGenerateTSTypeAllNumericTypes(t *testing.T) {
numericTypes := []string{
"int", "int8", "int16", "int32", "int64",
"uint", "uint8", "uint16", "uint32", "uint64",
"float32", "float64",
}
for _, goType := range numericTypes {
t.Run(goType, func(t *testing.T) {
result := autocode.GenerateTSType(goType)
if result != "number" {
t.Errorf("GenerateTSType(%s) = %s, 期望 number", goType, result)
}
})
}
}
// TestGenerateTSTypeMediaTypes 测试媒体类型映射
func TestGenerateTSTypeMediaTypes(t *testing.T) {
testCases := []struct {
goType string
expected string
}{
{"picture", "string"},
{"pictures", "string[]"},
{"video", "string"},
{"file", "Array<{ name: string; url: string; uid: string }>"},
{"richtext", "string"},
}
for _, tc := range testCases {
t.Run(tc.goType, func(t *testing.T) {
result := autocode.GenerateTSType(tc.goType)
if result != tc.expected {
t.Errorf("GenerateTSType(%s) = %s, 期望 %s", tc.goType, result, tc.expected)
}
})
}
}
// TestToPascalCase 测试 PascalCase 转换
func TestToPascalCase(t *testing.T) {
testCases := []struct {
input string
expected string
}{
{"article_status", "ArticleStatus"},
{"user-role", "UserRole"},
{"hello world", "HelloWorld"},
{"simple", "Simple"},
{"", ""},
{"already_Pascal_Case", "AlreadyPascalCase"},
}
for _, tc := range testCases {
result := autocode.ToPascalCase(tc.input)
if result != tc.expected {
t.Errorf("ToPascalCase(%s) = %s, 期望 %s", tc.input, result, tc.expected)
}
}
}
// TestGenerateModelField 测试 GORM Gen 模型字段生成
func TestGenerateModelField(t *testing.T) {
testCases := []struct {
name string
field autocode.AutoCodeField
contains []string // 生成的字段应包含的内容
}{
{
name: "字符串字段",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldDesc: "标题",
FieldType: "string",
FieldJson: "title",
ColumnName: "title",
DataTypeLong: "255",
},
contains: []string{"Title", "string", "gorm:", "column:title", "json:\"title\"", "标题"},
},
{
name: "整数字段",
field: autocode.AutoCodeField{
FieldName: "Status",
FieldDesc: "状态",
FieldType: "int",
FieldJson: "status",
ColumnName: "status",
DataTypeLong: "1",
},
contains: []string{"Status", "int8", "gorm:", "column:status", "json:\"status\""},
},
{
name: "时间字段",
field: autocode.AutoCodeField{
FieldName: "CreatedAt",
FieldDesc: "创建时间",
FieldType: "time.Time",
FieldJson: "createdAt",
ColumnName: "created_at",
},
contains: []string{"CreatedAt", "time.Time", "gorm:", "column:created_at"},
},
{
name: "布尔字段",
field: autocode.AutoCodeField{
FieldName: "IsActive",
FieldDesc: "是否激活",
FieldType: "bool",
FieldJson: "isActive",
ColumnName: "is_active",
},
contains: []string{"IsActive", "bool", "gorm:", "column:is_active"},
},
{
name: "JSON字段",
field: autocode.AutoCodeField{
FieldName: "Extra",
FieldDesc: "扩展信息",
FieldType: "json",
FieldJson: "extra",
ColumnName: "extra",
},
contains: []string{"Extra", "datatypes.JSON", "gorm:", "column:extra"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateModelField(tc.field)
for _, expected := range tc.contains {
if !strings.Contains(result, expected) {
t.Errorf("GenerateModelField 结果应包含 %s, 实际结果: %s", expected, result)
}
}
})
}
}
// TestGenerateGormGenSearchConditions 测试 GORM Gen 搜索条件生成
func TestGenerateGormGenSearchConditions(t *testing.T) {
fields := []*autocode.AutoCodeField{
{
FieldName: "Title",
FieldType: "string",
FieldSearchType: "LIKE",
},
{
FieldName: "Status",
FieldType: "int",
FieldSearchType: "EQ",
},
{
FieldName: "CreatedAt",
FieldType: "time.Time",
FieldSearchType: "BETWEEN",
},
}
result := autocode.GenerateGormGenSearchConditions(fields, "Article")
// 验证 LIKE 条件
if !strings.Contains(result, "t.Title.Like") {
t.Error("LIKE 搜索条件应使用 t.Title.Like")
}
// 验证 EQ 条件
if !strings.Contains(result, "t.Status.Eq") {
t.Error("EQ 搜索条件应使用 t.Status.Eq")
}
// 验证 BETWEEN 条件
if !strings.Contains(result, "t.CreatedAt.Between") {
t.Error("BETWEEN 搜索条件应使用 t.CreatedAt.Between")
}
}
// TestGenerateReactFormItem 测试 React 表单项生成
func TestGenerateReactFormItem(t *testing.T) {
testCases := []struct {
name string
field autocode.AutoCodeField
contains []string
}{
{
name: "字符串输入框",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldDesc: "标题",
FieldType: "string",
FieldJson: "title",
Require: true,
},
contains: []string{"Form.Item", "name=\"title\"", "label=\"标题\"", "Input", "required: true"},
},
{
name: "布尔开关",
field: autocode.AutoCodeField{
FieldName: "IsActive",
FieldDesc: "是否激活",
FieldType: "bool",
FieldJson: "isActive",
},
contains: []string{"Form.Item", "name=\"isActive\"", "Switch", "valuePropName=\"checked\""},
},
{
name: "数字输入框",
field: autocode.AutoCodeField{
FieldName: "Count",
FieldDesc: "数量",
FieldType: "int",
FieldJson: "count",
},
contains: []string{"Form.Item", "name=\"count\"", "InputNumber"},
},
{
name: "日期选择器",
field: autocode.AutoCodeField{
FieldName: "PublishDate",
FieldDesc: "发布日期",
FieldType: "time.Time",
FieldJson: "publishDate",
},
contains: []string{"Form.Item", "name=\"publishDate\"", "DatePicker", "showTime"},
},
{
name: "字典选择器",
field: autocode.AutoCodeField{
FieldName: "Status",
FieldDesc: "状态",
FieldType: "string",
FieldJson: "status",
DictType: "article_status",
},
contains: []string{"Form.Item", "name=\"status\"", "Select", "article_statusOptions"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateReactFormItem(tc.field)
for _, expected := range tc.contains {
if !strings.Contains(result, expected) {
t.Errorf("GenerateReactFormItem 结果应包含 %s, 实际结果: %s", expected, result)
}
}
})
}
}
// TestGenerateReactTableColumn 测试 React 表格列生成
func TestGenerateReactTableColumn(t *testing.T) {
testCases := []struct {
name string
field autocode.AutoCodeField
contains []string
}{
{
name: "普通字符串列",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldDesc: "标题",
FieldType: "string",
FieldJson: "title",
},
contains: []string{"title: '标题'", "dataIndex: 'title'", "key: 'title'"},
},
{
name: "可排序列",
field: autocode.AutoCodeField{
FieldName: "CreatedAt",
FieldDesc: "创建时间",
FieldType: "time.Time",
FieldJson: "createdAt",
Sort: true,
},
contains: []string{"title: '创建时间'", "sorter: true", "dayjs"},
},
{
name: "布尔列",
field: autocode.AutoCodeField{
FieldName: "IsActive",
FieldDesc: "是否激活",
FieldType: "bool",
FieldJson: "isActive",
},
contains: []string{"title: '是否激活'", "是", "否"},
},
{
name: "图片列",
field: autocode.AutoCodeField{
FieldName: "Cover",
FieldDesc: "封面",
FieldType: "picture",
FieldJson: "cover",
},
contains: []string{"title: '封面'", "Image"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateReactTableColumn(tc.field)
for _, expected := range tc.contains {
if !strings.Contains(result, expected) {
t.Errorf("GenerateReactTableColumn 结果应包含 %s, 实际结果: %s", expected, result)
}
}
})
}
}
// TestGenerateReactDefaultValue 测试 React 默认值生成
func TestGenerateReactDefaultValue(t *testing.T) {
testCases := []struct {
fieldType string
fieldJson string
expected string
}{
{"bool", "isActive", "isActive: false,"},
{"string", "title", "title: '',"},
{"int", "count", "count: 0,"},
{"float64", "price", "price: 0,"},
{"time.Time", "createdAt", "createdAt: undefined,"},
{"pictures", "images", "images: [],"},
{"file", "attachment", "attachment: [],"},
{"json", "extra", "extra: {},"},
}
for _, tc := range testCases {
field := autocode.AutoCodeField{
FieldType: tc.fieldType,
FieldJson: tc.fieldJson,
}
result := autocode.GenerateReactDefaultValue(field)
if result != tc.expected {
t.Errorf("GenerateReactDefaultValue(%s) = %s, 期望 %s", tc.fieldType, result, tc.expected)
}
}
}
// TestTemplateRenderingBasic 测试基本模板渲染功能
func TestTemplateRenderingBasic(t *testing.T) {
// 简单模板测试
tplContent := `package {{.Package}}
type {{.StructName}} struct {
ID int64
}
`
data := map[string]interface{}{
"Package": "example",
"StructName": "Article",
}
tmpl, err := template.New("test").Funcs(autocode.GetTemplateFuncMap()).Parse(tplContent)
if err != nil {
t.Fatalf("模板解析失败: %v", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
t.Fatalf("模板执行失败: %v", err)
}
result := buf.String()
// 验证渲染结果
if !strings.Contains(result, "package example") {
t.Error("渲染结果应包含 'package example'")
}
if !strings.Contains(result, "type Article struct") {
t.Error("渲染结果应包含 'type Article struct'")
}
}
// TestTemplateRenderingWithFunctions 测试带模板函数的渲染
func TestTemplateRenderingWithFunctions(t *testing.T) {
tplContent := `// {{.Description}}类型定义
export interface {{.StructName}} {
{{- range .Fields}}
{{.FieldJson}}: {{ GenerateTSType .FieldType }}; // {{.FieldDesc}}
{{- end}}
}
`
data := map[string]interface{}{
"Description": "文章",
"StructName": "Article",
"Fields": []autocode.AutoCodeField{
{FieldJson: "title", FieldType: "string", FieldDesc: "标题"},
{FieldJson: "content", FieldType: "string", FieldDesc: "内容"},
{FieldJson: "viewCount", FieldType: "int", FieldDesc: "浏览量"},
{FieldJson: "isPublished", FieldType: "bool", FieldDesc: "是否发布"},
},
}
tmpl, err := template.New("test").Funcs(autocode.GetTemplateFuncMap()).Parse(tplContent)
if err != nil {
t.Fatalf("模板解析失败: %v", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
t.Fatalf("模板执行失败: %v", err)
}
result := buf.String()
// 验证 TypeScript 类型映射
if !strings.Contains(result, "title: string;") {
t.Error("渲染结果应包含 'title: string;'")
}
if !strings.Contains(result, "viewCount: number;") {
t.Error("渲染结果应包含 'viewCount: number;'")
}
if !strings.Contains(result, "isPublished: boolean;") {
t.Error("渲染结果应包含 'isPublished: boolean;'")
}
}
// TestGoCodeFormatting 测试 Go 代码格式化(Property 4: 模板渲染幂等性)
func TestGoCodeFormatting(t *testing.T) {
// 生成的 Go 代码
goCode := `package example
import "fmt"
type Article struct {
ID int64
Title string
Content string
}
func (a *Article) String() string {
return fmt.Sprintf("Article{ID: %d, Title: %s}", a.ID, a.Title)
}
`
// 格式化代码
formatted, err := format.Source([]byte(goCode))
if err != nil {
t.Fatalf("代码格式化失败: %v", err)
}
// 再次格式化,验证幂等性
formattedAgain, err := format.Source(formatted)
if err != nil {
t.Fatalf("二次格式化失败: %v", err)
}
// 验证两次格式化结果相同
if string(formatted) != string(formattedAgain) {
t.Error("格式化应该是幂等的,两次格式化结果应相同")
}
}
// TestGenerateSearchField 测试搜索字段生成
func TestGenerateSearchField(t *testing.T) {
testCases := []struct {
name string
field autocode.AutoCodeField
contains []string
isEmpty bool
}{
{
name: "无搜索类型",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldType: "string",
FieldJson: "title",
FieldSearchType: "",
},
isEmpty: true,
},
{
name: "LIKE搜索",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldType: "string",
FieldJson: "title",
FieldSearchType: "LIKE",
},
contains: []string{"Title", "*string", "json:\"title\""},
},
{
name: "BETWEEN搜索-时间",
field: autocode.AutoCodeField{
FieldName: "CreatedAt",
FieldType: "time.Time",
FieldJson: "createdAt",
FieldSearchType: "BETWEEN",
},
contains: []string{"CreatedAtRange", "[]time.Time"},
},
{
name: "BETWEEN搜索-数字",
field: autocode.AutoCodeField{
FieldName: "Price",
FieldType: "float64",
FieldJson: "price",
FieldSearchType: "BETWEEN",
},
contains: []string{"StartPrice", "EndPrice", "*float64"},
},
{
name: "IN搜索",
field: autocode.AutoCodeField{
FieldName: "Status",
FieldType: "int",
FieldJson: "status",
FieldSearchType: "IN",
},
contains: []string{"Status", "[]int"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateSearchField(tc.field)
if tc.isEmpty {
if result != "" {
t.Errorf("期望空字符串,实际: %s", result)
}
return
}
for _, expected := range tc.contains {
if !strings.Contains(result, expected) {
t.Errorf("GenerateSearchField 结果应包含 %s, 实际结果: %s", expected, result)
}
}
})
}
}
// TestGenerateReactDescriptionItem 测试 React 描述项生成
func TestGenerateReactDescriptionItem(t *testing.T) {
testCases := []struct {
name string
field autocode.AutoCodeField
contains []string
}{
{
name: "普通字符串",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldDesc: "标题",
FieldType: "string",
FieldJson: "title",
},
contains: []string{"Descriptions.Item", "label=\"标题\"", "currentRecord?.title"},
},
{
name: "布尔类型",
field: autocode.AutoCodeField{
FieldName: "IsActive",
FieldDesc: "是否激活",
FieldType: "bool",
FieldJson: "isActive",
},
contains: []string{"Descriptions.Item", "是", "否"},
},
{
name: "时间类型",
field: autocode.AutoCodeField{
FieldName: "CreatedAt",
FieldDesc: "创建时间",
FieldType: "time.Time",
FieldJson: "createdAt",
},
contains: []string{"Descriptions.Item", "dayjs", "format"},
},
{
name: "图片类型",
field: autocode.AutoCodeField{
FieldName: "Cover",
FieldDesc: "封面",
FieldType: "picture",
FieldJson: "cover",
},
contains: []string{"Descriptions.Item", "Image"},
},
{
name: "JSON类型",
field: autocode.AutoCodeField{
FieldName: "Extra",
FieldDesc: "扩展信息",
FieldType: "json",
FieldJson: "extra",
},
contains: []string{"Descriptions.Item", "JSON.stringify"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateReactDescriptionItem(tc.field)
for _, expected := range tc.contains {
if !strings.Contains(result, expected) {
t.Errorf("GenerateReactDescriptionItem 结果应包含 %s, 实际结果: %s", expected, result)
}
}
})
}
}
// TestGenerateReactProTableColumn 测试 Ant Design Pro Table 列配置生成
func TestGenerateReactProTableColumn(t *testing.T) {
testCases := []struct {
name string
field autocode.AutoCodeField
contains []string
}{
{
name: "普通文本列",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldDesc: "标题",
FieldType: "string",
FieldJson: "title",
},
contains: []string{"title: '标题'", "dataIndex: 'title'", "ellipsis: true"},
},
{
name: "布尔列",
field: autocode.AutoCodeField{
FieldName: "IsActive",
FieldDesc: "是否激活",
FieldType: "bool",
FieldJson: "isActive",
},
contains: []string{"valueType: 'select'", "valueEnum", "是", "否"},
},
{
name: "时间列-范围搜索",
field: autocode.AutoCodeField{
FieldName: "CreatedAt",
FieldDesc: "创建时间",
FieldType: "time.Time",
FieldJson: "createdAt",
FieldSearchType: "BETWEEN",
},
contains: []string{"valueType: 'dateTimeRange'"},
},
{
name: "数字列",
field: autocode.AutoCodeField{
FieldName: "Count",
FieldDesc: "数量",
FieldType: "int",
FieldJson: "count",
FieldSearchType: "EQ",
},
contains: []string{"valueType: 'digit'"},
},
{
name: "字典列",
field: autocode.AutoCodeField{
FieldName: "Status",
FieldDesc: "状态",
FieldType: "string",
FieldJson: "status",
DictType: "article_status",
},
contains: []string{"valueType: 'select'", "article_statusOptions"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateReactProTableColumn(tc.field)
for _, expected := range tc.contains {
if !strings.Contains(result, expected) {
t.Errorf("GenerateReactProTableColumn 结果应包含 %s, 实际结果: %s", expected, result)
}
}
})
}
}
// TestComplexTemplateRendering 测试复杂模板渲染(模拟实际 biz 层模板)
func TestComplexTemplateRendering(t *testing.T) {
tplContent := `package {{.Package}}
import (
"context"
"{{.Module}}/internal/data/model/{{.Package}}"
"github.com/go-kratos/kratos/v2/log"
)
// {{.StructName}}Repo {{.Description}}仓储接口
type {{.StructName}}Repo interface {
Create(ctx context.Context, {{.Abbreviation}} *{{.Package}}.{{.StructName}}) error
Delete(ctx context.Context, {{.PrimaryField.FieldJson}} {{.PrimaryField.FieldType}}) error
Update(ctx context.Context, {{.Abbreviation}} *{{.Package}}.{{.StructName}}) error
FindByID(ctx context.Context, {{.PrimaryField.FieldJson}} {{.PrimaryField.FieldType}}) (*{{.Package}}.{{.StructName}}, error)
}
// {{.StructName}}Usecase {{.Description}}用例
type {{.StructName}}Usecase struct {
repo {{.StructName}}Repo
log *log.Helper
}
`
data := map[string]interface{}{
"Package": "article",
"Module": "kra",
"StructName": "Article",
"Description": "文章",
"Abbreviation": "article",
"PrimaryField": map[string]interface{}{
"FieldJson": "id",
"FieldType": "int64",
},
}
tmpl, err := template.New("test").Funcs(autocode.GetTemplateFuncMap()).Parse(tplContent)
if err != nil {
t.Fatalf("模板解析失败: %v", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
t.Fatalf("模板执行失败: %v", err)
}
result := buf.String()
// 验证关键内容
expectedContents := []string{
"package article",
"ArticleRepo interface",
"ArticleUsecase struct",
"Create(ctx context.Context",
"Delete(ctx context.Context",
"Update(ctx context.Context",
"FindByID(ctx context.Context",
"*article.Article",
"log.Helper",
}
for _, expected := range expectedContents {
if !strings.Contains(result, expected) {
t.Errorf("渲染结果应包含 '%s'", expected)
}
}
}
+5
View File
@@ -71,6 +71,11 @@ func (uc *CasbinUsecase) RemoveFilteredPolicy(authorityId string) error {
return uc.repo.RemoveFilteredPolicy(authorityId)
}
// SyncPolicy 同步策略(先删除再添加)
func (uc *CasbinUsecase) SyncPolicy(authorityId string, rules [][]string) error {
return uc.repo.SyncPolicy(authorityId, rules)
}
// UpdateCasbinApi API更新随动
func (uc *CasbinUsecase) UpdateCasbinApi(oldPath, newPath, oldMethod, newMethod string) error {
return uc.repo.UpdateCasbinApi(oldPath, newPath, oldMethod, newMethod)
+224
View File
@@ -0,0 +1,224 @@
package system
import (
"context"
"database/sql"
"errors"
"fmt"
"sort"
"github.com/go-kratos/kratos/v2/log"
"gorm.io/gorm"
)
const (
Mysql = "mysql"
Pgsql = "pgsql"
Sqlite = "sqlite"
Mssql = "mssql"
InitSuccess = "\n[%v] --> 初始数据成功!\n"
InitDataExist = "\n[%v] --> %v 的初始数据已存在!\n"
InitDataFailed = "\n[%v] --> %v 初始数据失败! \nerr: %+v\n"
InitDataSuccess = "\n[%v] --> %v 初始数据成功!\n"
)
const (
InitOrderSystem = 10
InitOrderInternal = 1000
InitOrderExternal = 100000
)
var (
ErrMissingDBContext = errors.New("missing db in context")
ErrMissingDependentContext = errors.New("missing dependent value in context")
ErrDBTypeMismatch = errors.New("db type mismatch")
ErrNoInitializers = errors.New("无可用初始化过程,请检查初始化是否已执行完成")
)
// InitDBRequest 初始化数据库请求
type InitDBRequest struct {
AdminPassword string `json:"adminPassword" binding:"required"`
DBType string `json:"dbType"` // 数据库类型
Host string `json:"host"` // 服务器地址
Port string `json:"port"` // 数据库连接端口
UserName string `json:"userName"` // 数据库用户名
Password string `json:"password"` // 数据库密码
DBName string `json:"dbName" binding:"required"` // 数据库名
DBPath string `json:"dbPath"` // sqlite数据库文件路径
Template string `json:"template"` // postgresql指定template
}
// SubInitializer 提供 source/*/init() 使用的接口,每个 initializer 完成一个初始化过程
type SubInitializer interface {
InitializerName() string // 不一定代表单独一个表,所以改成了更宽泛的语义
MigrateTable(ctx context.Context) (next context.Context, err error)
InitializeData(ctx context.Context) (next context.Context, err error)
TableCreated(ctx context.Context) bool
DataInserted(ctx context.Context) bool
}
// TypedDBInitHandler 执行传入的 initializer
type TypedDBInitHandler interface {
EnsureDB(ctx context.Context, conf *InitDBRequest) (context.Context, error) // 建库,失败属于 fatal error
WriteConfig(ctx context.Context) error // 回写配置
InitTables(ctx context.Context, inits InitSlice) error // 建表 handler
InitData(ctx context.Context, inits InitSlice) error // 建数据 handler
}
// orderedInitializer 组合一个顺序字段,以供排序
type orderedInitializer struct {
order int
SubInitializer
}
// InitSlice 供 initializer 排序依赖时使用
type InitSlice []*orderedInitializer
// InitDBUsecase 数据库初始化用例
type InitDBUsecase struct {
log *log.Helper
initializers InitSlice
cache map[string]*orderedInitializer
dbSetter func(*gorm.DB) // 用于设置全局DB的回调
configWriter func(ctx context.Context, dbType string, config interface{}) error
}
// NewInitDBUsecase 创建数据库初始化用例
func NewInitDBUsecase(logger log.Logger) *InitDBUsecase {
return &InitDBUsecase{
log: log.NewHelper(logger),
initializers: InitSlice{},
cache: map[string]*orderedInitializer{},
}
}
// SetDBSetter 设置数据库设置回调
func (uc *InitDBUsecase) SetDBSetter(setter func(*gorm.DB)) {
uc.dbSetter = setter
}
// SetConfigWriter 设置配置写入回调
func (uc *InitDBUsecase) SetConfigWriter(writer func(ctx context.Context, dbType string, config interface{}) error) {
uc.configWriter = writer
}
// RegisterInit 注册要执行的初始化过程,会在 InitDB() 时调用
func (uc *InitDBUsecase) RegisterInit(order int, i SubInitializer) {
name := i.InitializerName()
if _, existed := uc.cache[name]; existed {
panic(fmt.Sprintf("Name conflict on %s", name))
}
ni := orderedInitializer{order, i}
uc.initializers = append(uc.initializers, &ni)
uc.cache[name] = &ni
}
// InitDB 创建数据库并初始化 总入口
func (uc *InitDBUsecase) InitDB(ctx context.Context, conf *InitDBRequest) error {
ctx = context.WithValue(ctx, "adminPassword", conf.AdminPassword)
if len(uc.initializers) == 0 {
return ErrNoInitializers
}
sort.Sort(&uc.initializers) // 保证有依赖的 initializer 排在后面执行
var initHandler TypedDBInitHandler
switch conf.DBType {
case "mysql":
initHandler = NewMysqlInitHandler(uc.log, uc.configWriter)
ctx = context.WithValue(ctx, "dbtype", "mysql")
case "pgsql":
initHandler = NewPgsqlInitHandler(uc.log, uc.configWriter)
ctx = context.WithValue(ctx, "dbtype", "pgsql")
case "sqlite":
initHandler = NewSqliteInitHandler(uc.log, uc.configWriter)
ctx = context.WithValue(ctx, "dbtype", "sqlite")
case "mssql":
initHandler = NewMssqlInitHandler(uc.log, uc.configWriter)
ctx = context.WithValue(ctx, "dbtype", "mssql")
default:
initHandler = NewMysqlInitHandler(uc.log, uc.configWriter)
ctx = context.WithValue(ctx, "dbtype", "mysql")
}
var err error
ctx, err = initHandler.EnsureDB(ctx, conf)
if err != nil {
return err
}
db := ctx.Value("db").(*gorm.DB)
if uc.dbSetter != nil {
uc.dbSetter(db)
}
if err = initHandler.InitTables(ctx, uc.initializers); err != nil {
return err
}
if err = initHandler.InitData(ctx, uc.initializers); err != nil {
return err
}
if err = initHandler.WriteConfig(ctx); err != nil {
return err
}
// 清空初始化器
uc.initializers = InitSlice{}
uc.cache = map[string]*orderedInitializer{}
return nil
}
// CheckDB 检查数据库是否需要初始化
func (uc *InitDBUsecase) CheckDB(ctx context.Context) bool {
return len(uc.initializers) > 0
}
// createDatabase 创建数据库( EnsureDB() 中调用 )
func createDatabase(dsn string, driver string, createSql string) error {
db, err := sql.Open(driver, dsn)
if err != nil {
return err
}
defer func(db *sql.DB) {
err = db.Close()
if err != nil {
fmt.Println(err)
}
}(db)
if err = db.Ping(); err != nil {
return err
}
_, err = db.Exec(createSql)
return err
}
// createTables 创建表(默认 dbInitHandler.initTables 行为)
func createTables(ctx context.Context, inits InitSlice) error {
next, cancel := context.WithCancel(ctx)
defer cancel()
for _, init := range inits {
if init.TableCreated(next) {
continue
}
if n, err := init.MigrateTable(next); err != nil {
return err
} else {
next = n
}
}
return nil
}
/* -- sortable interface -- */
func (a InitSlice) Len() int {
return len(a)
}
func (a InitSlice) Less(i, j int) bool {
return a[i].order < a[j].order
}
func (a InitSlice) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
+132
View File
@@ -0,0 +1,132 @@
package system
import (
"context"
"errors"
"path/filepath"
"github.com/go-kratos/kratos/v2/log"
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
)
// MssqlConfig MSSQL配置
type MssqlConfig struct {
Path string
Port string
Config string
Dbname string
Username string
Password string
MaxIdleConns int
MaxOpenConns int
LogMode string
}
// Dsn 生成MSSQL DSN
func (c *MssqlConfig) Dsn() string {
return "sqlserver://" + c.Username + ":" + c.Password + "@" + c.Path + ":" + c.Port + "?database=" + c.Dbname + "&encrypt=disable"
}
// MssqlInitHandler MSSQL初始化处理器
type MssqlInitHandler struct {
log *log.Helper
configWriter func(ctx context.Context, dbType string, config interface{}) error
}
// NewMssqlInitHandler 创建MSSQL初始化处理器
func NewMssqlInitHandler(logger *log.Helper, configWriter func(ctx context.Context, dbType string, config interface{}) error) *MssqlInitHandler {
return &MssqlInitHandler{
log: logger,
configWriter: configWriter,
}
}
// WriteConfig mssql回写配置
func (h *MssqlInitHandler) WriteConfig(ctx context.Context) error {
c, ok := ctx.Value("config").(MssqlConfig)
if !ok {
return errors.New("mssql config invalid")
}
if h.configWriter != nil {
return h.configWriter(ctx, "mssql", c)
}
return nil
}
// EnsureDB 创建数据库并初始化 mssql
func (h *MssqlInitHandler) EnsureDB(ctx context.Context, conf *InitDBRequest) (next context.Context, err error) {
if s, ok := ctx.Value("dbtype").(string); !ok || s != "mssql" {
return ctx, ErrDBTypeMismatch
}
c := h.toMssqlConfig(conf)
next = context.WithValue(ctx, "config", c)
if c.Dbname == "" {
return ctx, nil
} // 如果没有数据库名, 则跳出初始化数据
dsn := h.mssqlEmptyDsn(conf)
mssqlConfig := sqlserver.Config{
DSN: dsn, // DSN data source name
DefaultStringSize: 191, // string 类型字段的默认长度
}
var db *gorm.DB
if db, err = gorm.Open(sqlserver.New(mssqlConfig), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil {
return nil, err
}
// 设置AutoCode根目录
autoCodeRoot, _ := filepath.Abs("..")
next = context.WithValue(next, "autoCodeRoot", autoCodeRoot)
next = context.WithValue(next, "db", db)
return next, err
}
// InitTables 初始化表
func (h *MssqlInitHandler) InitTables(ctx context.Context, inits InitSlice) error {
return createTables(ctx, inits)
}
// InitData 初始化数据
func (h *MssqlInitHandler) InitData(ctx context.Context, inits InitSlice) error {
next, cancel := context.WithCancel(ctx)
defer cancel()
for _, init := range inits {
if init.DataInserted(next) {
h.log.Infof(InitDataExist, Mssql, init.InitializerName())
continue
}
if n, err := init.InitializeData(next); err != nil {
h.log.Errorf(InitDataFailed, Mssql, init.InitializerName(), err)
return err
} else {
next = n
h.log.Infof(InitDataSuccess, Mssql, init.InitializerName())
}
}
h.log.Infof(InitSuccess, Mssql)
return nil
}
// mssqlEmptyDsn mssql 空数据库 建库链接
func (h *MssqlInitHandler) mssqlEmptyDsn(i *InitDBRequest) string {
return "sqlserver://" + i.UserName + ":" + i.Password + "@" + i.Host + ":" + i.Port + "?database=" + i.DBName + "&encrypt=disable"
}
// toMssqlConfig 转换为MSSQL配置
func (h *MssqlInitHandler) toMssqlConfig(i *InitDBRequest) MssqlConfig {
return MssqlConfig{
Path: i.Host,
Port: i.Port,
Dbname: i.DBName,
Username: i.UserName,
Password: i.Password,
MaxIdleConns: 10,
MaxOpenConns: 100,
LogMode: "error",
}
}
+144
View File
@@ -0,0 +1,144 @@
package system
import (
"context"
"errors"
"fmt"
"path/filepath"
"github.com/go-kratos/kratos/v2/log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// MysqlConfig MySQL配置
type MysqlConfig struct {
Path string
Port string
Config string
Dbname string
Username string
Password string
MaxIdleConns int
MaxOpenConns int
LogMode string
}
// Dsn 生成MySQL DSN
func (c *MysqlConfig) Dsn() string {
return c.Username + ":" + c.Password + "@tcp(" + c.Path + ":" + c.Port + ")/" + c.Dbname + "?" + c.Config
}
// MysqlInitHandler MySQL初始化处理器
type MysqlInitHandler struct {
log *log.Helper
configWriter func(ctx context.Context, dbType string, config interface{}) error
}
// NewMysqlInitHandler 创建MySQL初始化处理器
func NewMysqlInitHandler(logger *log.Helper, configWriter func(ctx context.Context, dbType string, config interface{}) error) *MysqlInitHandler {
return &MysqlInitHandler{
log: logger,
configWriter: configWriter,
}
}
// WriteConfig mysql回写配置
func (h *MysqlInitHandler) WriteConfig(ctx context.Context) error {
c, ok := ctx.Value("config").(MysqlConfig)
if !ok {
return errors.New("mysql config invalid")
}
if h.configWriter != nil {
return h.configWriter(ctx, "mysql", c)
}
return nil
}
// EnsureDB 创建数据库并初始化 mysql
func (h *MysqlInitHandler) EnsureDB(ctx context.Context, conf *InitDBRequest) (next context.Context, err error) {
if s, ok := ctx.Value("dbtype").(string); !ok || s != "mysql" {
return ctx, ErrDBTypeMismatch
}
c := h.toMysqlConfig(conf)
next = context.WithValue(ctx, "config", c)
if c.Dbname == "" {
return ctx, nil
} // 如果没有数据库名, 则跳出初始化数据
dsn := h.mysqlEmptyDsn(conf)
createSql := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;", c.Dbname)
if err = createDatabase(dsn, "mysql", createSql); err != nil {
return nil, err
} // 创建数据库
var db *gorm.DB
if db, err = gorm.Open(mysql.New(mysql.Config{
DSN: c.Dsn(), // DSN data source name
DefaultStringSize: 191, // string 类型字段的默认长度
SkipInitializeWithVersion: true, // 根据版本自动配置
}), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil {
return ctx, err
}
// 设置AutoCode根目录
autoCodeRoot, _ := filepath.Abs("..")
next = context.WithValue(next, "autoCodeRoot", autoCodeRoot)
next = context.WithValue(next, "db", db)
return next, err
}
// InitTables 初始化表
func (h *MysqlInitHandler) InitTables(ctx context.Context, inits InitSlice) error {
return createTables(ctx, inits)
}
// InitData 初始化数据
func (h *MysqlInitHandler) InitData(ctx context.Context, inits InitSlice) error {
next, cancel := context.WithCancel(ctx)
defer cancel()
for _, init := range inits {
if init.DataInserted(next) {
h.log.Infof(InitDataExist, Mysql, init.InitializerName())
continue
}
if n, err := init.InitializeData(next); err != nil {
h.log.Errorf(InitDataFailed, Mysql, init.InitializerName(), err)
return err
} else {
next = n
h.log.Infof(InitDataSuccess, Mysql, init.InitializerName())
}
}
h.log.Infof(InitSuccess, Mysql)
return nil
}
// mysqlEmptyDsn mysql 空数据库 建库链接
func (h *MysqlInitHandler) mysqlEmptyDsn(i *InitDBRequest) string {
host := i.Host
port := i.Port
if host == "" {
host = "127.0.0.1"
}
if port == "" {
port = "3306"
}
return fmt.Sprintf("%s:%s@tcp(%s:%s)/", i.UserName, i.Password, host, port)
}
// toMysqlConfig 转换为MySQL配置
func (h *MysqlInitHandler) toMysqlConfig(i *InitDBRequest) MysqlConfig {
return MysqlConfig{
Path: i.Host,
Port: i.Port,
Dbname: i.DBName,
Username: i.UserName,
Password: i.Password,
MaxIdleConns: 10,
MaxOpenConns: 100,
LogMode: "error",
Config: "charset=utf8mb4&parseTime=True&loc=Local",
}
}
+148
View File
@@ -0,0 +1,148 @@
package system
import (
"context"
"errors"
"fmt"
"path/filepath"
"github.com/go-kratos/kratos/v2/log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
// PgsqlConfig PostgreSQL配置
type PgsqlConfig struct {
Path string
Port string
Config string
Dbname string
Username string
Password string
MaxIdleConns int
MaxOpenConns int
LogMode string
}
// Dsn 生成PostgreSQL DSN
func (c *PgsqlConfig) Dsn() string {
return "host=" + c.Path + " user=" + c.Username + " password=" + c.Password + " dbname=" + c.Dbname + " port=" + c.Port + " " + c.Config
}
// PgsqlInitHandler PostgreSQL初始化处理器
type PgsqlInitHandler struct {
log *log.Helper
configWriter func(ctx context.Context, dbType string, config interface{}) error
}
// NewPgsqlInitHandler 创建PostgreSQL初始化处理器
func NewPgsqlInitHandler(logger *log.Helper, configWriter func(ctx context.Context, dbType string, config interface{}) error) *PgsqlInitHandler {
return &PgsqlInitHandler{
log: logger,
configWriter: configWriter,
}
}
// WriteConfig pgsql 回写配置
func (h *PgsqlInitHandler) WriteConfig(ctx context.Context) error {
c, ok := ctx.Value("config").(PgsqlConfig)
if !ok {
return errors.New("postgresql config invalid")
}
if h.configWriter != nil {
return h.configWriter(ctx, "pgsql", c)
}
return nil
}
// EnsureDB 创建数据库并初始化 pg
func (h *PgsqlInitHandler) EnsureDB(ctx context.Context, conf *InitDBRequest) (next context.Context, err error) {
if s, ok := ctx.Value("dbtype").(string); !ok || s != "pgsql" {
return ctx, ErrDBTypeMismatch
}
c := h.toPgsqlConfig(conf)
next = context.WithValue(ctx, "config", c)
if c.Dbname == "" {
return ctx, nil
} // 如果没有数据库名, 则跳出初始化数据
dsn := h.pgsqlEmptyDsn(conf)
var createSql string
if conf.Template != "" {
createSql = fmt.Sprintf("CREATE DATABASE %s WITH TEMPLATE %s;", c.Dbname, conf.Template)
} else {
createSql = fmt.Sprintf("CREATE DATABASE %s;", c.Dbname)
}
if err = createDatabase(dsn, "pgx", createSql); err != nil {
return nil, err
} // 创建数据库
var db *gorm.DB
if db, err = gorm.Open(postgres.New(postgres.Config{
DSN: c.Dsn(), // DSN data source name
PreferSimpleProtocol: false,
}), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil {
return ctx, err
}
// 设置AutoCode根目录
autoCodeRoot, _ := filepath.Abs("..")
next = context.WithValue(next, "autoCodeRoot", autoCodeRoot)
next = context.WithValue(next, "db", db)
return next, err
}
// InitTables 初始化表
func (h *PgsqlInitHandler) InitTables(ctx context.Context, inits InitSlice) error {
return createTables(ctx, inits)
}
// InitData 初始化数据
func (h *PgsqlInitHandler) InitData(ctx context.Context, inits InitSlice) error {
next, cancel := context.WithCancel(ctx)
defer cancel()
for i := 0; i < len(inits); i++ {
if inits[i].DataInserted(next) {
h.log.Infof(InitDataExist, Pgsql, inits[i].InitializerName())
continue
}
if n, err := inits[i].InitializeData(next); err != nil {
h.log.Errorf(InitDataFailed, Pgsql, inits[i].InitializerName(), err)
return err
} else {
next = n
h.log.Infof(InitDataSuccess, Pgsql, inits[i].InitializerName())
}
}
h.log.Infof(InitSuccess, Pgsql)
return nil
}
// pgsqlEmptyDsn pgsql 空数据库 建库链接
func (h *PgsqlInitHandler) pgsqlEmptyDsn(i *InitDBRequest) string {
host := i.Host
port := i.Port
if host == "" {
host = "127.0.0.1"
}
if port == "" {
port = "5432"
}
return "host=" + host + " user=" + i.UserName + " password=" + i.Password + " port=" + port + " dbname=" + "postgres" + " " + "sslmode=disable TimeZone=Asia/Shanghai"
}
// toPgsqlConfig 转换为PostgreSQL配置
func (h *PgsqlInitHandler) toPgsqlConfig(i *InitDBRequest) PgsqlConfig {
return PgsqlConfig{
Path: i.Host,
Port: i.Port,
Dbname: i.DBName,
Username: i.UserName,
Password: i.Password,
MaxIdleConns: 10,
MaxOpenConns: 100,
LogMode: "error",
Config: "sslmode=disable TimeZone=Asia/Shanghai",
}
}
+123
View File
@@ -0,0 +1,123 @@
package system
import (
"context"
"errors"
"os"
"path/filepath"
"github.com/glebarez/sqlite"
"github.com/go-kratos/kratos/v2/log"
"gorm.io/gorm"
)
// SqliteConfig SQLite配置
type SqliteConfig struct {
Path string
Dbname string
MaxIdleConns int
MaxOpenConns int
LogMode string
}
// Dsn 生成SQLite DSN
func (c *SqliteConfig) Dsn() string {
return filepath.Join(c.Path, c.Dbname+".db")
}
// SqliteInitHandler SQLite初始化处理器
type SqliteInitHandler struct {
log *log.Helper
configWriter func(ctx context.Context, dbType string, config interface{}) error
}
// NewSqliteInitHandler 创建SQLite初始化处理器
func NewSqliteInitHandler(logger *log.Helper, configWriter func(ctx context.Context, dbType string, config interface{}) error) *SqliteInitHandler {
return &SqliteInitHandler{
log: logger,
configWriter: configWriter,
}
}
// WriteConfig sqlite回写配置
func (h *SqliteInitHandler) WriteConfig(ctx context.Context) error {
c, ok := ctx.Value("config").(SqliteConfig)
if !ok {
return errors.New("sqlite config invalid")
}
if h.configWriter != nil {
return h.configWriter(ctx, "sqlite", c)
}
return nil
}
// EnsureDB 创建数据库并初始化 sqlite
func (h *SqliteInitHandler) EnsureDB(ctx context.Context, conf *InitDBRequest) (next context.Context, err error) {
if s, ok := ctx.Value("dbtype").(string); !ok || s != "sqlite" {
return ctx, ErrDBTypeMismatch
}
c := h.toSqliteConfig(conf)
next = context.WithValue(ctx, "config", c)
if c.Dbname == "" {
return ctx, nil
} // 如果没有数据库名, 则跳出初始化数据
dsn := h.sqliteEmptyDsn(conf)
var db *gorm.DB
if db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
}); err != nil {
return ctx, err
}
// 设置AutoCode根目录
autoCodeRoot, _ := filepath.Abs("..")
next = context.WithValue(next, "autoCodeRoot", autoCodeRoot)
next = context.WithValue(next, "db", db)
return next, err
}
// InitTables 初始化表
func (h *SqliteInitHandler) InitTables(ctx context.Context, inits InitSlice) error {
return createTables(ctx, inits)
}
// InitData 初始化数据
func (h *SqliteInitHandler) InitData(ctx context.Context, inits InitSlice) error {
next, cancel := context.WithCancel(ctx)
defer cancel()
for _, init := range inits {
if init.DataInserted(next) {
h.log.Infof(InitDataExist, Sqlite, init.InitializerName())
continue
}
if n, err := init.InitializeData(next); err != nil {
h.log.Errorf(InitDataFailed, Sqlite, init.InitializerName(), err)
return err
} else {
next = n
h.log.Infof(InitDataSuccess, Sqlite, init.InitializerName())
}
}
h.log.Infof(InitSuccess, Sqlite)
return nil
}
// sqliteEmptyDsn sqlite 空数据库 建库链接
func (h *SqliteInitHandler) sqliteEmptyDsn(i *InitDBRequest) string {
separator := string(os.PathSeparator)
return i.DBPath + separator + i.DBName + ".db"
}
// toSqliteConfig 转换为SQLite配置
func (h *SqliteInitHandler) toSqliteConfig(i *InitDBRequest) SqliteConfig {
return SqliteConfig{
Path: i.DBPath,
Dbname: i.DBName,
MaxIdleConns: 10,
MaxOpenConns: 100,
LogMode: "error",
}
}
+23 -1
View File
@@ -1,6 +1,10 @@
package system
import "context"
import (
"context"
"github.com/redis/go-redis/v9"
)
// JwtBlacklist JWT黑名单实体
type JwtBlacklist struct {
@@ -13,6 +17,8 @@ type JwtBlacklistRepo interface {
Create(ctx context.Context, jwt string) error
IsInBlacklist(ctx context.Context, jwt string) bool
LoadAll(ctx context.Context) ([]string, error)
GetRedisJWT(ctx context.Context, username string) (string, error)
SetRedisJWT(ctx context.Context, token, username string) error
}
// JwtBlacklistUsecase JWT黑名单用例
@@ -68,3 +74,19 @@ func (uc *JwtBlacklistUsecase) LoadAll(ctx context.Context) error {
}
return nil
}
// GetRedisJWT 从Redis获取JWT
func (uc *JwtBlacklistUsecase) GetRedisJWT(ctx context.Context, username string) (string, error) {
if uc.repo == nil {
return "", redis.Nil
}
return uc.repo.GetRedisJWT(ctx, username)
}
// SetRedisJWT 设置Redis JWT
func (uc *JwtBlacklistUsecase) SetRedisJWT(ctx context.Context, token, username string) error {
if uc.repo == nil {
return nil
}
return uc.repo.SetRedisJWT(ctx, token, username)
}
+986
View File
@@ -0,0 +1,986 @@
package system
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// getProjectRoot returns the project root directory
func getProjectRoot() string {
_, filename, _, _ := runtime.Caller(0)
// Go up from internal/biz/system to project root
return filepath.Join(filepath.Dir(filename), "..", "..", "..")
}
// Feature: gva-to-kratos-migration, Property 1: Migration Completeness
// *For any* GVA source file containing functions, the corresponding KRA target file
// SHALL contain equivalent functions with matching signatures.
// **Validates: Requirements 1.1, 1.2, 2.1, 2.2, 3.1, 3.2**
// TestMigrationCompleteness_ServiceLayer tests that all GVA service functions are migrated to KRA biz layer
func TestMigrationCompleteness_ServiceLayer(t *testing.T) {
root := getProjectRoot()
// Define mapping from GVA service files to KRA biz files
serviceMappings := map[string]string{
filepath.Join(root, "gva/server/service/system/sys_user.go"): filepath.Join(root, "internal/biz/system/user.go"),
filepath.Join(root, "gva/server/service/system/sys_api.go"): filepath.Join(root, "internal/biz/system/api.go"),
filepath.Join(root, "gva/server/service/system/sys_authority.go"): filepath.Join(root, "internal/biz/system/authority.go"),
filepath.Join(root, "gva/server/service/system/sys_menu.go"): filepath.Join(root, "internal/biz/system/menu.go"),
filepath.Join(root, "gva/server/service/system/sys_base_menu.go"): filepath.Join(root, "internal/biz/system/menu.go"),
filepath.Join(root, "gva/server/service/system/sys_casbin.go"): filepath.Join(root, "internal/biz/system/casbin.go"),
filepath.Join(root, "gva/server/service/system/sys_dictionary.go"): filepath.Join(root, "internal/biz/system/dictionary.go"),
filepath.Join(root, "gva/server/service/system/sys_dictionary_detail.go"): filepath.Join(root, "internal/biz/system/dictionary.go"),
filepath.Join(root, "gva/server/service/system/sys_operation_record.go"): filepath.Join(root, "internal/biz/system/operation_record.go"),
filepath.Join(root, "gva/server/service/system/jwt_black_list.go"): filepath.Join(root, "internal/biz/system/jwt_blacklist.go"),
filepath.Join(root, "gva/server/service/system/sys_export_template.go"): filepath.Join(root, "internal/biz/system/export_template.go"),
filepath.Join(root, "gva/server/service/system/sys_params.go"): filepath.Join(root, "internal/biz/system/params.go"),
filepath.Join(root, "gva/server/service/system/sys_system.go"): filepath.Join(root, "internal/biz/system/system.go"),
filepath.Join(root, "gva/server/service/system/sys_version.go"): filepath.Join(root, "internal/biz/system/version.go"),
filepath.Join(root, "gva/server/service/system/sys_error.go"): filepath.Join(root, "internal/biz/system/error.go"),
filepath.Join(root, "gva/server/service/system/sys_authority_btn.go"): filepath.Join(root, "internal/biz/system/authority_btn.go"),
}
for gvaFile, kraFile := range serviceMappings {
t.Run(filepath.Base(gvaFile), func(t *testing.T) {
gvaFuncs := extractPublicFunctions(t, gvaFile)
kraFuncs := extractPublicFunctions(t, kraFile)
// Check that KRA file exists
if _, err := os.Stat(kraFile); os.IsNotExist(err) {
t.Errorf("KRA file %s does not exist for GVA file %s", kraFile, gvaFile)
return
}
// For each GVA function, check if equivalent exists in KRA
for funcName := range gvaFuncs {
// Skip internal/helper functions and struct methods that may have different names
if strings.HasPrefix(funcName, "get") || strings.HasPrefix(funcName, "set") {
continue
}
// Check if function exists (may have different naming convention)
found := false
for kraFunc := range kraFuncs {
if strings.EqualFold(funcName, kraFunc) || strings.Contains(strings.ToLower(kraFunc), strings.ToLower(funcName)) {
found = true
break
}
}
if !found {
t.Logf("Function %s from %s may need migration to %s", funcName, gvaFile, kraFile)
}
}
// Verify KRA file has functions
assert.NotEmpty(t, kraFuncs, "KRA file %s should have functions", kraFile)
})
}
}
// TestMigrationCompleteness_AutoCodeLayer tests AutoCode functionality migration
func TestMigrationCompleteness_AutoCodeLayer(t *testing.T) {
root := getProjectRoot()
autoCodeMappings := map[string]string{
filepath.Join(root, "gva/server/service/system/auto_code_template.go"): filepath.Join(root, "internal/biz/system/auto_code_template.go"),
filepath.Join(root, "gva/server/service/system/auto_code_package.go"): filepath.Join(root, "internal/biz/system/auto_code_package.go"),
filepath.Join(root, "gva/server/service/system/auto_code_history.go"): filepath.Join(root, "internal/biz/system/auto_code_history.go"),
filepath.Join(root, "gva/server/service/system/auto_code_plugin.go"): filepath.Join(root, "internal/biz/system/auto_code_plugin.go"),
filepath.Join(root, "gva/server/service/system/auto_code_llm.go"): filepath.Join(root, "internal/biz/system/auto_code_llm.go"),
filepath.Join(root, "gva/server/service/system/auto_code_mcp.go"): filepath.Join(root, "internal/biz/system/auto_code_mcp.go"),
}
for gvaFile, kraFile := range autoCodeMappings {
t.Run(filepath.Base(gvaFile), func(t *testing.T) {
// Check KRA file exists
if _, err := os.Stat(kraFile); os.IsNotExist(err) {
t.Errorf("KRA AutoCode file %s does not exist", kraFile)
return
}
kraFuncs := extractPublicFunctions(t, kraFile)
assert.NotEmpty(t, kraFuncs, "KRA AutoCode file %s should have functions", kraFile)
})
}
}
// TestMigrationCompleteness_InitDBLayer tests InitDB functionality migration
func TestMigrationCompleteness_InitDBLayer(t *testing.T) {
root := getProjectRoot()
initDBMappings := map[string]string{
filepath.Join(root, "gva/server/service/system/sys_initdb.go"): filepath.Join(root, "internal/biz/system/initdb.go"),
filepath.Join(root, "gva/server/service/system/sys_initdb_mysql.go"): filepath.Join(root, "internal/biz/system/initdb_mysql.go"),
filepath.Join(root, "gva/server/service/system/sys_initdb_pgsql.go"): filepath.Join(root, "internal/biz/system/initdb_pgsql.go"),
}
for gvaFile, kraFile := range initDBMappings {
t.Run(filepath.Base(gvaFile), func(t *testing.T) {
if _, err := os.Stat(kraFile); os.IsNotExist(err) {
t.Errorf("KRA InitDB file %s does not exist", kraFile)
return
}
kraFuncs := extractPublicFunctions(t, kraFile)
assert.NotEmpty(t, kraFuncs, "KRA InitDB file %s should have functions", kraFile)
})
}
}
// extractPublicFunctions extracts all public function names from a Go file
func extractPublicFunctions(t *testing.T, filePath string) map[string]bool {
t.Helper()
funcs := make(map[string]bool)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return funcs
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
t.Logf("Warning: could not parse %s: %v", filePath, err)
return funcs
}
ast.Inspect(node, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.FuncDecl:
// Only include public functions (starting with uppercase)
if x.Name != nil && len(x.Name.Name) > 0 {
firstChar := x.Name.Name[0]
if firstChar >= 'A' && firstChar <= 'Z' {
funcs[x.Name.Name] = true
}
}
}
return true
})
return funcs
}
// Feature: gva-to-kratos-migration, Property 2: Logic Preservation
// *For any* migrated function, given the same input parameters, the function
// SHALL produce equivalent output (accounting for framework-specific adaptations).
// **Validates: Requirements 1.3, 2.3, 4.2, 5.4**
// TestLogicPreservation_FunctionSignatures tests that function signatures are preserved
func TestLogicPreservation_FunctionSignatures(t *testing.T) {
root := getProjectRoot()
// Test cases for function signature preservation
testCases := []struct {
name string
gvaFile string
kraFile string
funcNames []string // Functions that should have similar signatures
}{
{
name: "User Service Functions",
gvaFile: filepath.Join(root, "gva/server/service/system/sys_user.go"),
kraFile: filepath.Join(root, "internal/biz/system/user.go"),
funcNames: []string{"Register", "Login", "ChangePassword", "ResetPassword"},
},
{
name: "API Service Functions",
gvaFile: filepath.Join(root, "gva/server/service/system/sys_api.go"),
kraFile: filepath.Join(root, "internal/biz/system/api.go"),
funcNames: []string{"CreateApi", "DeleteApi", "UpdateApi"},
},
{
name: "Authority Service Functions",
gvaFile: filepath.Join(root, "gva/server/service/system/sys_authority.go"),
kraFile: filepath.Join(root, "internal/biz/system/authority.go"),
funcNames: []string{"CreateAuthority", "DeleteAuthority", "UpdateAuthority"},
},
{
name: "Menu Service Functions",
gvaFile: filepath.Join(root, "gva/server/service/system/sys_menu.go"),
kraFile: filepath.Join(root, "internal/biz/system/menu.go"),
funcNames: []string{"AddBaseMenu", "DeleteBaseMenu", "UpdateBaseMenu"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gvaFuncs := extractFunctionSignatures(t, tc.gvaFile)
kraFuncs := extractFunctionSignatures(t, tc.kraFile)
for _, funcName := range tc.funcNames {
gvaSig, gvaExists := gvaFuncs[funcName]
kraSig, kraExists := kraFuncs[funcName]
if gvaExists && kraExists {
// Both exist, log for comparison
t.Logf("Function %s - GVA params: %d, KRA params: %d", funcName, gvaSig.paramCount, kraSig.paramCount)
} else if gvaExists && !kraExists {
// Check if there's a similar function with different naming
found := false
for kraFunc := range kraFuncs {
if strings.Contains(strings.ToLower(kraFunc), strings.ToLower(funcName)) {
found = true
t.Logf("Function %s found as %s in KRA", funcName, kraFunc)
break
}
}
if !found {
t.Logf("Function %s exists in GVA but not found in KRA", funcName)
}
}
}
// Verify KRA file has functions
assert.NotEmpty(t, kraFuncs, "KRA file should have functions")
})
}
}
// TestLogicPreservation_MethodReceivers tests that method receivers are properly converted
func TestLogicPreservation_MethodReceivers(t *testing.T) {
root := getProjectRoot()
// In GVA, services are struct methods. In KRA, they should be Usecase methods
kraFiles := []string{
filepath.Join(root, "internal/biz/system/user.go"),
filepath.Join(root, "internal/biz/system/api.go"),
filepath.Join(root, "internal/biz/system/authority.go"),
filepath.Join(root, "internal/biz/system/menu.go"),
filepath.Join(root, "internal/biz/system/casbin.go"),
}
for _, kraFile := range kraFiles {
t.Run(filepath.Base(kraFile), func(t *testing.T) {
if _, err := os.Stat(kraFile); os.IsNotExist(err) {
t.Skipf("KRA file %s does not exist", kraFile)
return
}
methods := extractMethodReceivers(t, kraFile)
// Check that methods have proper receivers (Usecase pattern)
hasUsecaseReceiver := false
for receiver := range methods {
if strings.Contains(receiver, "Usecase") || strings.Contains(receiver, "UseCase") {
hasUsecaseReceiver = true
break
}
}
// Log the receivers found
for receiver, count := range methods {
t.Logf("Receiver %s has %d methods", receiver, count)
}
// KRA should use Usecase pattern or similar DI pattern
if !hasUsecaseReceiver && len(methods) > 0 {
t.Logf("Note: %s may not follow Usecase pattern, found receivers: %v", kraFile, methods)
}
})
}
}
// TestLogicPreservation_ErrorHandling tests that error handling patterns are preserved
func TestLogicPreservation_ErrorHandling(t *testing.T) {
root := getProjectRoot()
// Check that KRA biz layer has proper error definitions
errorFile := filepath.Join(root, "internal/biz/system/error.go")
if _, err := os.Stat(errorFile); os.IsNotExist(err) {
// Check alternative location
errorFile = filepath.Join(root, "internal/biz/errros.go")
}
if _, err := os.Stat(errorFile); os.IsNotExist(err) {
t.Skip("Error definition file not found")
return
}
// Parse the error file and check for error definitions
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, errorFile, nil, parser.ParseComments)
if err != nil {
t.Fatalf("Failed to parse error file: %v", err)
}
errorCount := 0
ast.Inspect(node, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.ValueSpec:
// Check for error variable declarations
for _, name := range x.Names {
if strings.HasPrefix(name.Name, "Err") || strings.HasPrefix(name.Name, "err") {
errorCount++
}
}
}
return true
})
t.Logf("Found %d error definitions in %s", errorCount, errorFile)
assert.Greater(t, errorCount, 0, "Should have error definitions for proper error handling")
}
// funcSignature holds basic function signature info
type funcSignature struct {
paramCount int
returnCount int
}
// extractFunctionSignatures extracts function signatures from a Go file
func extractFunctionSignatures(t *testing.T, filePath string) map[string]funcSignature {
t.Helper()
sigs := make(map[string]funcSignature)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return sigs
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
t.Logf("Warning: could not parse %s: %v", filePath, err)
return sigs
}
ast.Inspect(node, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.FuncDecl:
if x.Name != nil && len(x.Name.Name) > 0 {
firstChar := x.Name.Name[0]
if firstChar >= 'A' && firstChar <= 'Z' {
sig := funcSignature{}
if x.Type.Params != nil {
sig.paramCount = len(x.Type.Params.List)
}
if x.Type.Results != nil {
sig.returnCount = len(x.Type.Results.List)
}
sigs[x.Name.Name] = sig
}
}
}
return true
})
return sigs
}
// extractMethodReceivers extracts method receivers and their counts
func extractMethodReceivers(t *testing.T, filePath string) map[string]int {
t.Helper()
receivers := make(map[string]int)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return receivers
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
t.Logf("Warning: could not parse %s: %v", filePath, err)
return receivers
}
ast.Inspect(node, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.FuncDecl:
if x.Recv != nil && len(x.Recv.List) > 0 {
// Get receiver type name
for _, field := range x.Recv.List {
switch t := field.Type.(type) {
case *ast.StarExpr:
if ident, ok := t.X.(*ast.Ident); ok {
receivers[ident.Name]++
}
case *ast.Ident:
receivers[t.Name]++
}
}
}
}
return true
})
return receivers
}
// Feature: gva-to-kratos-migration, Property 3: Dependency Injection Compliance
// *For any* KRA component that previously used global variables in GVA,
// the component SHALL receive dependencies through constructor injection.
// **Validates: Requirements 1.4, 4.3, 14.1**
// TestDependencyInjection_NoGlobalVariables tests that KRA biz layer doesn't use global variables
func TestDependencyInjection_NoGlobalVariables(t *testing.T) {
root := getProjectRoot()
// List of KRA biz files to check
bizFiles := []string{
filepath.Join(root, "internal/biz/system/user.go"),
filepath.Join(root, "internal/biz/system/api.go"),
filepath.Join(root, "internal/biz/system/authority.go"),
filepath.Join(root, "internal/biz/system/menu.go"),
filepath.Join(root, "internal/biz/system/casbin.go"),
filepath.Join(root, "internal/biz/system/dictionary.go"),
filepath.Join(root, "internal/biz/system/operation_record.go"),
filepath.Join(root, "internal/biz/system/jwt_blacklist.go"),
}
// GVA global variable patterns to check for
globalPatterns := []string{
"global.GVA_DB",
"global.GVA_CONFIG",
"global.GVA_LOG",
"global.GVA_REDIS",
"global.BlackCache",
"global.GVA_ROUTERS",
}
for _, bizFile := range bizFiles {
t.Run(filepath.Base(bizFile), func(t *testing.T) {
if _, err := os.Stat(bizFile); os.IsNotExist(err) {
t.Skipf("File %s does not exist", bizFile)
return
}
content, err := os.ReadFile(bizFile)
if err != nil {
t.Fatalf("Failed to read file: %v", err)
}
fileContent := string(content)
for _, pattern := range globalPatterns {
if strings.Contains(fileContent, pattern) {
t.Errorf("File %s contains global variable reference: %s", bizFile, pattern)
}
}
})
}
}
// TestDependencyInjection_ConstructorPattern tests that KRA uses constructor injection
func TestDependencyInjection_ConstructorPattern(t *testing.T) {
root := getProjectRoot()
// List of KRA biz files that should have New* constructors
bizFiles := []string{
filepath.Join(root, "internal/biz/system/user.go"),
filepath.Join(root, "internal/biz/system/api.go"),
filepath.Join(root, "internal/biz/system/authority.go"),
filepath.Join(root, "internal/biz/system/menu.go"),
filepath.Join(root, "internal/biz/system/casbin.go"),
}
for _, bizFile := range bizFiles {
t.Run(filepath.Base(bizFile), func(t *testing.T) {
if _, err := os.Stat(bizFile); os.IsNotExist(err) {
t.Skipf("File %s does not exist", bizFile)
return
}
constructors := extractConstructors(t, bizFile)
// Should have at least one New* constructor
assert.NotEmpty(t, constructors, "File %s should have constructor functions", bizFile)
// Log constructors found
for name, params := range constructors {
t.Logf("Constructor %s has %d parameters", name, params)
}
})
}
}
// TestDependencyInjection_RepoInterface tests that KRA uses repository interfaces
func TestDependencyInjection_RepoInterface(t *testing.T) {
root := getProjectRoot()
// List of KRA biz files that should define repository interfaces
bizFiles := []string{
filepath.Join(root, "internal/biz/system/user.go"),
filepath.Join(root, "internal/biz/system/api.go"),
filepath.Join(root, "internal/biz/system/authority.go"),
filepath.Join(root, "internal/biz/system/menu.go"),
}
for _, bizFile := range bizFiles {
t.Run(filepath.Base(bizFile), func(t *testing.T) {
if _, err := os.Stat(bizFile); os.IsNotExist(err) {
t.Skipf("File %s does not exist", bizFile)
return
}
interfaces := extractInterfaces(t, bizFile)
// Check for Repo interface pattern
hasRepoInterface := false
for name := range interfaces {
if strings.Contains(name, "Repo") || strings.Contains(name, "Repository") {
hasRepoInterface = true
t.Logf("Found repository interface: %s", name)
}
}
if !hasRepoInterface && len(interfaces) > 0 {
t.Logf("Note: %s has interfaces but no Repo pattern: %v", bizFile, interfaces)
}
})
}
}
// TestDependencyInjection_WireProviderSet tests that wire provider sets exist
func TestDependencyInjection_WireProviderSet(t *testing.T) {
root := getProjectRoot()
// Check for wire.go files
wireFiles := []string{
filepath.Join(root, "cmd/kratos-admin/wire.go"),
}
for _, wireFile := range wireFiles {
t.Run(filepath.Base(wireFile), func(t *testing.T) {
if _, err := os.Stat(wireFile); os.IsNotExist(err) {
t.Skipf("Wire file %s does not exist", wireFile)
return
}
content, err := os.ReadFile(wireFile)
if err != nil {
t.Fatalf("Failed to read wire file: %v", err)
}
fileContent := string(content)
// Check for wire.Build or wire.NewSet
hasWireBuild := strings.Contains(fileContent, "wire.Build") || strings.Contains(fileContent, "wire.NewSet")
assert.True(t, hasWireBuild, "Wire file should contain wire.Build or wire.NewSet")
// Check for provider imports
hasProviderImport := strings.Contains(fileContent, "google.golang.org/wire") || strings.Contains(fileContent, "github.com/google/wire")
assert.True(t, hasProviderImport, "Wire file should import wire package")
})
}
}
// TestDependencyInjection_UsecaseStructure tests that Usecase structs have proper dependencies
func TestDependencyInjection_UsecaseStructure(t *testing.T) {
root := getProjectRoot()
bizFiles := []string{
filepath.Join(root, "internal/biz/system/user.go"),
filepath.Join(root, "internal/biz/system/api.go"),
filepath.Join(root, "internal/biz/system/authority.go"),
}
for _, bizFile := range bizFiles {
t.Run(filepath.Base(bizFile), func(t *testing.T) {
if _, err := os.Stat(bizFile); os.IsNotExist(err) {
t.Skipf("File %s does not exist", bizFile)
return
}
structs := extractStructFields(t, bizFile)
// Check for Usecase structs with dependencies
for structName, fields := range structs {
if strings.Contains(structName, "Usecase") || strings.Contains(structName, "UseCase") {
t.Logf("Usecase struct %s has %d fields", structName, len(fields))
// Should have at least one dependency field (repo, logger, etc.)
assert.NotEmpty(t, fields, "Usecase %s should have dependency fields", structName)
for _, field := range fields {
t.Logf(" - Field: %s", field)
}
}
}
})
}
}
// extractConstructors extracts New* constructor functions and their parameter counts
func extractConstructors(t *testing.T, filePath string) map[string]int {
t.Helper()
constructors := make(map[string]int)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return constructors
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
t.Logf("Warning: could not parse %s: %v", filePath, err)
return constructors
}
ast.Inspect(node, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.FuncDecl:
if x.Name != nil && strings.HasPrefix(x.Name.Name, "New") {
paramCount := 0
if x.Type.Params != nil {
paramCount = len(x.Type.Params.List)
}
constructors[x.Name.Name] = paramCount
}
}
return true
})
return constructors
}
// extractInterfaces extracts interface definitions from a Go file
func extractInterfaces(t *testing.T, filePath string) map[string]int {
t.Helper()
interfaces := make(map[string]int)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return interfaces
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
t.Logf("Warning: could not parse %s: %v", filePath, err)
return interfaces
}
ast.Inspect(node, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.TypeSpec:
if _, ok := x.Type.(*ast.InterfaceType); ok {
interfaces[x.Name.Name] = 1
}
}
return true
})
return interfaces
}
// extractStructFields extracts struct definitions and their field names
func extractStructFields(t *testing.T, filePath string) map[string][]string {
t.Helper()
structs := make(map[string][]string)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return structs
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
t.Logf("Warning: could not parse %s: %v", filePath, err)
return structs
}
ast.Inspect(node, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.TypeSpec:
if structType, ok := x.Type.(*ast.StructType); ok {
var fields []string
if structType.Fields != nil {
for _, field := range structType.Fields.List {
for _, name := range field.Names {
fields = append(fields, name.Name)
}
}
}
structs[x.Name.Name] = fields
}
}
return true
})
return structs
}
// Feature: gva-to-kratos-migration, Property 4: Import Integrity
// *For any* KRA source file, all import statements SHALL resolve to valid packages
// and the project SHALL compile without import errors.
// **Validates: Requirements 16.1, 16.2, 16.3, 16.4**
// TestImportIntegrity_NoGVAReferences tests that KRA files don't reference GVA packages
func TestImportIntegrity_NoGVAReferences(t *testing.T) {
root := getProjectRoot()
// Directories to check for GVA references
dirsToCheck := []string{
filepath.Join(root, "internal/biz/system"),
filepath.Join(root, "internal/data/system"),
filepath.Join(root, "internal/server/handler/system"),
filepath.Join(root, "internal/server/router/system"),
filepath.Join(root, "internal/server/middleware"),
filepath.Join(root, "pkg"),
}
// GVA import patterns that should not exist in KRA
gvaImportPatterns := []string{
"gin-vue-admin",
"github.com/flipped-aurora/gin-vue-admin",
"gva/server",
}
for _, dir := range dirsToCheck {
if _, err := os.Stat(dir); os.IsNotExist(err) {
continue
}
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() || !strings.HasSuffix(path, ".go") {
return nil
}
// Skip test files
if strings.HasSuffix(path, "_test.go") {
return nil
}
t.Run(filepath.Base(path), func(t *testing.T) {
imports := extractImports(t, path)
for _, imp := range imports {
for _, pattern := range gvaImportPatterns {
if strings.Contains(imp, pattern) {
t.Errorf("File %s has GVA import reference: %s", path, imp)
}
}
}
})
return nil
})
if err != nil {
t.Logf("Warning: error walking directory %s: %v", dir, err)
}
}
}
// TestImportIntegrity_KRAPackageNaming tests that KRA uses correct package naming
func TestImportIntegrity_KRAPackageNaming(t *testing.T) {
root := getProjectRoot()
// Check that KRA files use "kra" module name
goModFile := filepath.Join(root, "go.mod")
content, err := os.ReadFile(goModFile)
if err != nil {
t.Fatalf("Failed to read go.mod: %v", err)
}
// Check module name
assert.Contains(t, string(content), "module kra", "go.mod should declare module as 'kra'")
}
// TestImportIntegrity_InternalImports tests that internal packages use correct import paths
func TestImportIntegrity_InternalImports(t *testing.T) {
root := getProjectRoot()
// Files to check for correct internal imports
filesToCheck := []string{
filepath.Join(root, "internal/biz/system/user.go"),
filepath.Join(root, "internal/biz/system/api.go"),
filepath.Join(root, "internal/server/handler/system/sys_user.go"),
filepath.Join(root, "internal/server/router/system/sys_user.go"),
}
for _, file := range filesToCheck {
if _, err := os.Stat(file); os.IsNotExist(err) {
continue
}
t.Run(filepath.Base(file), func(t *testing.T) {
imports := extractImports(t, file)
// Check that internal imports use "kra/" prefix
for _, imp := range imports {
if strings.Contains(imp, "internal/") {
assert.True(t, strings.HasPrefix(imp, "kra/"),
"Internal import %s should use 'kra/' prefix", imp)
}
}
})
}
}
// TestImportIntegrity_NoCircularImports tests for potential circular import issues
func TestImportIntegrity_NoCircularImports(t *testing.T) {
root := getProjectRoot()
// Build import graph for key packages
packages := map[string][]string{
"biz": {},
"data": {},
"handler": {},
"router": {},
}
// Check biz layer imports
bizDir := filepath.Join(root, "internal/biz/system")
if _, err := os.Stat(bizDir); err == nil {
filepath.Walk(bizDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
imports := extractImports(t, path)
packages["biz"] = append(packages["biz"], imports...)
return nil
})
}
// Check data layer imports
dataDir := filepath.Join(root, "internal/data/system")
if _, err := os.Stat(dataDir); err == nil {
filepath.Walk(dataDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
imports := extractImports(t, path)
packages["data"] = append(packages["data"], imports...)
return nil
})
}
// Verify layering: biz should not import data directly (data implements biz interfaces)
for _, imp := range packages["biz"] {
if strings.Contains(imp, "internal/data") {
t.Logf("Note: biz layer imports data layer: %s (may be intentional for model types)", imp)
}
}
// Verify handler imports biz but not data directly
handlerDir := filepath.Join(root, "internal/server/handler/system")
if _, err := os.Stat(handlerDir); err == nil {
filepath.Walk(handlerDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
imports := extractImports(t, path)
for _, imp := range imports {
if strings.Contains(imp, "internal/data/system") {
t.Logf("Note: handler imports data layer directly: %s in %s", imp, path)
}
}
return nil
})
}
}
// TestImportIntegrity_CompilationCheck tests that the project compiles successfully
func TestImportIntegrity_CompilationCheck(t *testing.T) {
// This test verifies that the test file itself compiles,
// which implicitly tests that all imports in the package are valid
// If we reach this point, the package compiled successfully
t.Log("Package compiled successfully - all imports are valid")
}
// TestImportIntegrity_PackageDeclarations tests that package declarations are correct
func TestImportIntegrity_PackageDeclarations(t *testing.T) {
root := getProjectRoot()
// Map of directories to expected package names
packageExpectations := map[string]string{
filepath.Join(root, "internal/biz/system"): "system",
filepath.Join(root, "internal/data/system"): "system",
filepath.Join(root, "internal/server/handler/system"): "system",
filepath.Join(root, "internal/server/router/system"): "system",
filepath.Join(root, "internal/server/middleware"): "middleware",
filepath.Join(root, "pkg/utils"): "utils",
filepath.Join(root, "pkg/jwt"): "jwt",
filepath.Join(root, "pkg/auth"): "auth",
}
for dir, expectedPkg := range packageExpectations {
if _, err := os.Stat(dir); os.IsNotExist(err) {
continue
}
t.Run(filepath.Base(dir), func(t *testing.T) {
// Find first .go file in directory
entries, err := os.ReadDir(dir)
if err != nil {
t.Skipf("Cannot read directory %s: %v", dir, err)
return
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") {
continue
}
filePath := filepath.Join(dir, entry.Name())
pkgName := extractPackageName(t, filePath)
assert.Equal(t, expectedPkg, pkgName,
"File %s should have package name '%s', got '%s'", filePath, expectedPkg, pkgName)
break // Only check first file
}
})
}
}
// extractImports extracts all import paths from a Go file
func extractImports(t *testing.T, filePath string) []string {
t.Helper()
var imports []string
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return imports
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filePath, nil, parser.ImportsOnly)
if err != nil {
t.Logf("Warning: could not parse %s: %v", filePath, err)
return imports
}
for _, imp := range node.Imports {
if imp.Path != nil {
// Remove quotes from import path
path := strings.Trim(imp.Path.Value, "\"")
imports = append(imports, path)
}
}
return imports
}
// extractPackageName extracts the package name from a Go file
func extractPackageName(t *testing.T, filePath string) string {
t.Helper()
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return ""
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filePath, nil, parser.PackageClauseOnly)
if err != nil {
t.Logf("Warning: could not parse %s: %v", filePath, err)
return ""
}
if node.Name != nil {
return node.Name.Name
}
return ""
}