mengstack-api/cmd/smrm/main.go
MengStack Dev df809f1045
Some checks failed
CI / Build & Test (push) Failing after 1m31s
feat: user CRUD API + dashboard stats + kernel infrastructure
- 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
2026-10-03 03:42:58 +08:00

443 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
),
)
`