后端重构
This commit is contained in:
@@ -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用例
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user