package main import ( "fmt" "go/ast" "go/parser" "go/token" "os" "path/filepath" "strings" "text/template" ) func main() { if len(os.Args) < 2 { printUsage() os.Exit(1) } switch os.Args[1] { case "new": if len(os.Args) < 4 || os.Args[2] != "module" { fmt.Println("用法: smrm new module ") os.Exit(1) } createModule(os.Args[3]) case "check": runCheck() default: printUsage() os.Exit(1) } } func printUsage() { fmt.Println("smrm — MengStack 脚手架工具") fmt.Println() fmt.Println("用法:") fmt.Println(" smrm new module 创建标准四层模块骨架") fmt.Println(" smrm check 规范自检(扫描常见违规)") } func createModule(name string) { base := filepath.Join("internal", "modules", name) dirs := []string{"domain", "application", "infrastructure", "interfaces"} for _, d := range dirs { path := filepath.Join(base, d) if err := os.MkdirAll(path, 0755); err != nil { fmt.Fprintf(os.Stderr, "创建目录失败: %v\n", err) os.Exit(1) } } files := map[string]string{ filepath.Join(base, "domain", "model.go"): modelTemplate, filepath.Join(base, "domain", "repository.go"): repoTemplate, filepath.Join(base, "application", "service.go"): serviceTemplate, filepath.Join(base, "infrastructure", "repo.go"): infraRepoTemplate, filepath.Join(base, "interfaces", "handler.go"): handlerTemplate, filepath.Join(base, "interfaces", "plugin.go"): pluginTemplate, } data := templateData{Name: name, Title: strings.Title(name)} for path, tmpl := range files { if err := writeTemplate(path, tmpl, data); err != nil { fmt.Fprintf(os.Stderr, "生成文件失败 %s: %v\n", path, err) os.Exit(1) } } fmt.Printf("✅ 模块 %s 已创建\n", name) fmt.Println(" 下一步:") fmt.Printf(" 1. 在 internal/app/app.go 注册 %sinterfaces.Module\n", name) fmt.Printf(" 2. 在 newEngine() 中调用 %s 插件的 SetupRoutes\n", name) fmt.Println(" 3. 实现业务逻辑") } type templateData struct { Name string Title string } func writeTemplate(path, tmplStr string, data templateData) error { tmpl, err := template.New("").Parse(tmplStr) if err != nil { return err } f, err := os.Create(path) if err != nil { return err } defer f.Close() return tmpl.Execute(f, data) } func runCheck() { violations := 0 fmt.Println("🔍 MengStack 规范自检") fmt.Println("=====================") err := filepath.Walk("internal/kernel", func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") { return err } v := checkKernelImports(path) violations += len(v) for _, msg := range v { fmt.Printf(" ❌ %s\n", msg) } return nil }) if err != nil { fmt.Fprintf(os.Stderr, "扫描失败: %v\n", err) os.Exit(1) } err = filepath.Walk("internal/modules", func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") { return err } if strings.Contains(path, string(filepath.Separator)+"domain"+string(filepath.Separator)) { v := checkDomainImports(path) violations += len(v) for _, msg := range v { fmt.Printf(" ❌ %s\n", msg) } } return nil }) if err != nil { fmt.Fprintf(os.Stderr, "扫描失败: %v\n", err) os.Exit(1) } if violations == 0 { fmt.Println(" ✅ 未发现违规项") } else { fmt.Printf("\n 共发现 %d 处违规\n", violations) os.Exit(1) } } var forbiddenKernelImports = []string{ "mengstack/internal/modules", "mengstack/internal/apps", "mengstack/internal/middle", } var forbiddenDomainImports = []string{ "gorm.io/gorm", "github.com/gin-gonic/gin", "github.com/redis/go-redis", } func checkKernelImports(path string) []string { return checkForbidden(path, forbiddenKernelImports, "kernel 层禁止 import 业务模块") } func checkDomainImports(path string) []string { return checkForbidden(path, forbiddenDomainImports, "domain 层禁止 import 外部依赖") } func checkForbidden(path string, forbidden []string, reason string) []string { fset := token.NewFileSet() f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) if err != nil { return nil } var violations []string for _, imp := range f.Imports { importPath := strings.Trim(imp.Path.Value, `"`) for _, fb := range forbidden { if strings.HasPrefix(importPath, fb) { violations = append(violations, fmt.Sprintf("%s — %s(%s)", path, importPath, reason)) } } } return violations } func checkFileForAST(_ string) *ast.File { return nil } var modelTemplate = `package domain import "time" // {{.Title}} 领域实体。 type {{.Title}} struct { ID string ` + "`" + `json:"id"` + "`" + ` TenantID string ` + "`" + `json:"tenant_id"` + "`" + ` CreatedAt time.Time ` + "`" + `json:"created_at"` + "`" + ` UpdatedAt time.Time ` + "`" + `json:"updated_at"` + "`" + ` } ` var repoTemplate = `package domain import "context" // Repository 定义 {{.Name}} 模块的数据访问接口。 type Repository interface { Create(ctx context.Context, item *{{.Title}}) error GetByID(ctx context.Context, tenantID, id string) (*{{.Title}}, error) List(ctx context.Context, tenantID string) ([]{{.Title}}, error) Delete(ctx context.Context, tenantID, id string) error } ` var serviceTemplate = `package application import ( "context" "mengstack/internal/modules/{{.Name}}/domain" ) // Service 编排 {{.Name}} 模块的业务逻辑。 type Service struct { repo domain.Repository } func NewService(repo domain.Repository) *Service { return &Service{repo: repo} } func (s *Service) Create(ctx context.Context, item *domain.{{.Title}}) error { return s.repo.Create(ctx, item) } func (s *Service) Get(ctx context.Context, tenantID, id string) (*domain.{{.Title}}, error) { return s.repo.GetByID(ctx, tenantID, id) } func (s *Service) List(ctx context.Context, tenantID string) ([]domain.{{.Title}}, error) { return s.repo.List(ctx, tenantID) } func (s *Service) Delete(ctx context.Context, tenantID, id string) error { return s.repo.Delete(ctx, tenantID, id) } ` var infraRepoTemplate = `package infrastructure import ( "context" "mengstack/internal/modules/{{.Name}}/domain" "gorm.io/gorm" ) type repository struct { db *gorm.DB } func NewRepository(db *gorm.DB) domain.Repository { return &repository{db: db} } func (r *repository) Create(ctx context.Context, item *domain.{{.Title}}) error { return r.db.WithContext(ctx).Create(item).Error } func (r *repository) GetByID(ctx context.Context, tenantID, id string) (*domain.{{.Title}}, error) { var item domain.{{.Title}} err := r.db.WithContext(ctx).Where("id = ? AND tenant_id = ?", id, tenantID).First(&item).Error if err != nil { return nil, err } return &item, nil } func (r *repository) List(ctx context.Context, tenantID string) ([]domain.{{.Title}}, error) { var items []domain.{{.Title}} err := r.db.WithContext(ctx).Where("tenant_id = ?", tenantID).Find(&items).Error return items, err } func (r *repository) Delete(ctx context.Context, tenantID, id string) error { return r.db.WithContext(ctx). Where("id = ? AND tenant_id = ?", id, tenantID). Delete(&domain.{{.Title}}{}).Error } ` var handlerTemplate = `package interfaces import ( "net/http" "mengstack/internal/kernel/response" "mengstack/internal/kernel/tenant" "mengstack/internal/modules/{{.Name}}/application" "mengstack/internal/modules/{{.Name}}/domain" "github.com/gin-gonic/gin" ) type Handler struct { svc *application.Service } func NewHandler(svc *application.Service) *Handler { return &Handler{svc: svc} } func (h *Handler) Create(c *gin.Context) { tenantID := tenant.MustFromContext(c.Request.Context()) var item domain.{{.Title}} if err := c.ShouldBindJSON(&item); err != nil { response.Fail(c, err) return } item.TenantID = tenantID if err := h.svc.Create(c.Request.Context(), &item); err != nil { response.Fail(c, err) return } response.OK(c, item) } func (h *Handler) List(c *gin.Context) { tenantID := tenant.MustFromContext(c.Request.Context()) items, err := h.svc.List(c.Request.Context(), tenantID) if err != nil { response.Fail(c, err) return } response.OK(c, items) } func (h *Handler) Get(c *gin.Context) { tenantID := tenant.MustFromContext(c.Request.Context()) id := c.Param("id") item, err := h.svc.Get(c.Request.Context(), tenantID, id) if err != nil { response.Fail(c, err) return } response.OK(c, item) } func (h *Handler) Delete(c *gin.Context) { tenantID := tenant.MustFromContext(c.Request.Context()) id := c.Param("id") if err := h.svc.Delete(c.Request.Context(), tenantID, id); err != nil { response.Fail(c, err) return } response.OK(c, gin.H{"deleted": true}) } // unused guard — 确保 http 包被引用 var _ = http.StatusOK ` var pluginTemplate = `package interfaces import ( "io/fs" "mengstack/internal/app/middleware" "mengstack/internal/kernel/plugin" "mengstack/internal/kernel/tenant" "mengstack/internal/modules/{{.Name}}/application" "mengstack/internal/modules/{{.Name}}/infrastructure" "github.com/gin-gonic/gin" "go.uber.org/fx" ) type {{.Title}}Plugin struct { handler *Handler } func NewPlugin(handler *Handler) *{{.Title}}Plugin { p := &{{.Title}}Plugin{handler: handler} plugin.Register(p) return p } func (p *{{.Title}}Plugin) Metadata() plugin.Metadata { return plugin.Metadata{ Name: "{{.Name}}", Version: "0.1.0", Description: "{{.Title}} 模块", } } func (p *{{.Title}}Plugin) Permissions() plugin.Permissions { return plugin.Permissions{ Database: []string{""}, Storage: false, Network: []string{}, Events: []string{}, Routes: []string{"/api/v1/{{.Name}}s", "/api/v1/{{.Name}}s/*"}, AdminPanel: false, } } func (p *{{.Title}}Plugin) FxOption() fx.Option { return fx.Module("{{.Name}}-plugin", fx.Provide( infrastructure.NewRepository, application.NewService, NewHandler, ), ) } func (p *{{.Title}}Plugin) SetupRoutes(engine *gin.Engine, authMW gin.HandlerFunc, tenantResolver tenant.Resolver) { g := engine.Group("/api/v1/{{.Name}}s", authMW, middleware.MultiTenant(tenantResolver)) g.POST("", p.handler.Create) g.GET("", p.handler.List) g.GET("/:id", p.handler.Get) g.DELETE("/:id", p.handler.Delete) } func (p *{{.Title}}Plugin) MigrationsFS() fs.FS { return nil } func (p *{{.Title}}Plugin) Init() error { return nil } var Module = fx.Module("{{.Name}}-plugin", fx.Provide( infrastructure.NewRepository, application.NewService, NewHandler, NewPlugin, ), ) `