Some checks failed
CI / Build & Test (push) Failing after 1m31s
- Add user management endpoints (list/create/get/update/delete) with pagination and search - Add dashboard stats endpoint with tenant/user/online counts and growth metrics - Add tenant resolver middleware for multi-tenant request scoping - Add i18n kernel with zh/en message files and AcceptLanguage middleware - Add WebSocket hub/handler for real-time communication - Add job scheduler kernel with cron support - Add plugin sandbox for isolated execution - Add storage kernel (local filesystem) - Add event bus kernel for pub/sub - Add cache kernel abstraction - Add database migration runner and version upgrade checker - Add rate limiting middleware with Redis backend - Add SQL migrations for rbac, audit_logs, settings, notifications, examples - Extend user repository with list/delete/count operations - Register all module routes with tenant resolver
443 lines
10 KiB
Go
443 lines
10 KiB
Go
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 <name>")
|
||
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 <name> 创建标准四层模块骨架")
|
||
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,
|
||
),
|
||
)
|
||
`
|