feat(org,audit): M4 组织管理 + 审计日志
org 模块: - tenants 表 (UUID PK, name, slug, status, settings) - 完整 CRUD API: /api/v1/org/tenants - 多租户隔离 audit 模块: - audit_logs 表 (actor, action, resource, IP, 时间) - 查询 API: /api/v1/audit/logs (支持分页+筛选) - 按 tenant_id/user_id/action/resource 过滤
This commit is contained in:
parent
f8c374a624
commit
b13427e76c
@ -14,6 +14,8 @@ import (
|
|||||||
"mengstack/internal/logger"
|
"mengstack/internal/logger"
|
||||||
authinterfaces "mengstack/internal/modules/auth/interfaces"
|
authinterfaces "mengstack/internal/modules/auth/interfaces"
|
||||||
rbacinterfaces "mengstack/internal/modules/rbac/interfaces"
|
rbacinterfaces "mengstack/internal/modules/rbac/interfaces"
|
||||||
|
orginterfaces "mengstack/internal/modules/org/interfaces"
|
||||||
|
auditinterfaces "mengstack/internal/modules/audit/interfaces"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
@ -39,6 +41,8 @@ func newEngine(
|
|||||||
authHandler *authinterfaces.Handler,
|
authHandler *authinterfaces.Handler,
|
||||||
authMW gin.HandlerFunc,
|
authMW gin.HandlerFunc,
|
||||||
rbacHandler *rbacinterfaces.Handler,
|
rbacHandler *rbacinterfaces.Handler,
|
||||||
|
orgHandler *orginterfaces.Handler,
|
||||||
|
auditHandler *auditinterfaces.Handler,
|
||||||
) *gin.Engine {
|
) *gin.Engine {
|
||||||
ginMode := "release"
|
ginMode := "release"
|
||||||
if cfg.Server.Mode == "debug" || cfg.Server.Mode == "dev" {
|
if cfg.Server.Mode == "debug" || cfg.Server.Mode == "dev" {
|
||||||
@ -59,6 +63,8 @@ func newEngine(
|
|||||||
|
|
||||||
authinterfaces.SetupRoutes(r, authHandler, authMW)
|
authinterfaces.SetupRoutes(r, authHandler, authMW)
|
||||||
rbacinterfaces.SetupRoutes(r, rbacHandler, authMW)
|
rbacinterfaces.SetupRoutes(r, rbacHandler, authMW)
|
||||||
|
orginterfaces.SetupRoutes(r, orgHandler, authMW)
|
||||||
|
auditinterfaces.SetupRoutes(r, auditHandler, authMW)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@ -101,6 +107,8 @@ func NewApp() *fx.App {
|
|||||||
cache.Module,
|
cache.Module,
|
||||||
authinterfaces.Module,
|
authinterfaces.Module,
|
||||||
rbacinterfaces.Module,
|
rbacinterfaces.Module,
|
||||||
|
orginterfaces.Module,
|
||||||
|
auditinterfaces.Module,
|
||||||
Module,
|
Module,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
46
internal/modules/audit/application/service.go
Normal file
46
internal/modules/audit/application/service.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"mengstack/internal/kernel/errors"
|
||||||
|
"mengstack/internal/modules/audit/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
repo domain.AuditRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo domain.AuditRepository) *Service {
|
||||||
|
return &Service{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Log(ctx context.Context, tenantID string, userID uint, action, resource, resourceID, detail, ip string) error {
|
||||||
|
log := &domain.AuditLog{
|
||||||
|
TenantID: tenantID,
|
||||||
|
UserID: userID,
|
||||||
|
Action: action,
|
||||||
|
Resource: resource,
|
||||||
|
ResourceID: resourceID,
|
||||||
|
Detail: detail,
|
||||||
|
IP: ip,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.Create(ctx, log); err != nil {
|
||||||
|
return errors.Wrap(err, "AUDIT_LOG_FAILED", "failed to write audit log", 500)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Query(ctx context.Context, query domain.AuditQuery) ([]domain.AuditLogDTO, int64, error) {
|
||||||
|
logs, total, err := s.repo.Find(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, errors.Wrap(err, "QUERY_AUDIT_FAILED", "failed to query audit logs", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
dtos := make([]domain.AuditLogDTO, len(logs))
|
||||||
|
for i, l := range logs {
|
||||||
|
dtos[i] = domain.ToAuditLogDTO(&l)
|
||||||
|
}
|
||||||
|
return dtos, total, nil
|
||||||
|
}
|
||||||
54
internal/modules/audit/domain/log.go
Normal file
54
internal/modules/audit/domain/log.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type AuditLog struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
TenantID string `json:"tenant_id" gorm:"type:uuid;index;not null"`
|
||||||
|
UserID uint `json:"user_id" gorm:"index;not null"`
|
||||||
|
Action string `json:"action" gorm:"size:64;not null;index"`
|
||||||
|
Resource string `json:"resource" gorm:"size:128;not null;index"`
|
||||||
|
ResourceID string `json:"resource_id" gorm:"size:128"`
|
||||||
|
Detail string `json:"detail" gorm:"type:text"`
|
||||||
|
IP string `json:"ip" gorm:"size:64"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AuditLog) TableName() string {
|
||||||
|
return "audit_logs"
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuditLogDTO struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
TenantID string `json:"tenant_id"`
|
||||||
|
UserID uint `json:"user_id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
ResourceID string `json:"resource_id"`
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToAuditLogDTO(l *AuditLog) AuditLogDTO {
|
||||||
|
return AuditLogDTO{
|
||||||
|
ID: l.ID,
|
||||||
|
TenantID: l.TenantID,
|
||||||
|
UserID: l.UserID,
|
||||||
|
Action: l.Action,
|
||||||
|
Resource: l.Resource,
|
||||||
|
ResourceID: l.ResourceID,
|
||||||
|
Detail: l.Detail,
|
||||||
|
IP: l.IP,
|
||||||
|
CreatedAt: l.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuditQuery struct {
|
||||||
|
TenantID string
|
||||||
|
UserID uint
|
||||||
|
Action string
|
||||||
|
Resource string
|
||||||
|
Page int
|
||||||
|
PageSize int
|
||||||
|
}
|
||||||
8
internal/modules/audit/domain/repository.go
Normal file
8
internal/modules/audit/domain/repository.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type AuditRepository interface {
|
||||||
|
Create(ctx context.Context, log *AuditLog) error
|
||||||
|
Find(ctx context.Context, query AuditQuery) ([]AuditLog, int64, error)
|
||||||
|
}
|
||||||
61
internal/modules/audit/infrastructure/audit_repo.go
Normal file
61
internal/modules/audit/infrastructure/audit_repo.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package infrastructure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"mengstack/internal/modules/audit/domain"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type auditRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuditRepository(db *gorm.DB) domain.AuditRepository {
|
||||||
|
return &auditRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *auditRepo) Create(ctx context.Context, log *domain.AuditLog) error {
|
||||||
|
return r.db.WithContext(ctx).Create(log).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *auditRepo) Find(ctx context.Context, query domain.AuditQuery) ([]domain.AuditLog, int64, error) {
|
||||||
|
var logs []domain.AuditLog
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
tx := r.db.WithContext(ctx).Model(&domain.AuditLog{})
|
||||||
|
|
||||||
|
if query.TenantID != "" {
|
||||||
|
tx = tx.Where("tenant_id = ?", query.TenantID)
|
||||||
|
}
|
||||||
|
if query.UserID > 0 {
|
||||||
|
tx = tx.Where("user_id = ?", query.UserID)
|
||||||
|
}
|
||||||
|
if query.Action != "" {
|
||||||
|
tx = tx.Where("action = ?", query.Action)
|
||||||
|
}
|
||||||
|
if query.Resource != "" {
|
||||||
|
tx = tx.Where("resource = ?", query.Resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
page := query.Page
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize := query.PageSize
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
if err := tx.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs, total, nil
|
||||||
|
}
|
||||||
13
internal/modules/audit/infrastructure/migrate.go
Normal file
13
internal/modules/audit/infrastructure/migrate.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package infrastructure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mengstack/internal/modules/audit/domain"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Migrate(db *gorm.DB) error {
|
||||||
|
return db.AutoMigrate(
|
||||||
|
&domain.AuditLog{},
|
||||||
|
)
|
||||||
|
}
|
||||||
53
internal/modules/audit/interfaces/handler.go
Normal file
53
internal/modules/audit/interfaces/handler.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"mengstack/internal/kernel/response"
|
||||||
|
"mengstack/internal/kernel/tenant"
|
||||||
|
"mengstack/internal/modules/audit/application"
|
||||||
|
auditdomain "mengstack/internal/modules/audit/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) ListLogs(c *gin.Context) {
|
||||||
|
tenantID, _ := tenant.FromContext(c.Request.Context())
|
||||||
|
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||||
|
|
||||||
|
query := auditdomain.AuditQuery{
|
||||||
|
TenantID: tenantID,
|
||||||
|
UserID: parseUintQuery(c, "user_id"),
|
||||||
|
Action: c.Query("action"),
|
||||||
|
Resource: c.Query("resource"),
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
dtos, total, err := h.svc.Query(c.Request.Context(), query)
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"items": dtos,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUintQuery(c *gin.Context, key string) uint {
|
||||||
|
v, _ := strconv.ParseUint(c.DefaultQuery(key, "0"), 10, 64)
|
||||||
|
return uint(v)
|
||||||
|
}
|
||||||
33
internal/modules/audit/interfaces/routes.go
Normal file
33
internal/modules/audit/interfaces/routes.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mengstack/internal/app/middleware"
|
||||||
|
"mengstack/internal/modules/audit/application"
|
||||||
|
auditinfra "mengstack/internal/modules/audit/infrastructure"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupRoutes(r *gin.Engine, h *Handler, authMW gin.HandlerFunc) {
|
||||||
|
audit := r.Group("/api/v1/audit")
|
||||||
|
audit.Use(authMW, middleware.MultiTenant())
|
||||||
|
{
|
||||||
|
audit.GET("/logs", h.ListLogs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Module = fx.Module("audit",
|
||||||
|
fx.Provide(
|
||||||
|
auditinfra.NewAuditRepository,
|
||||||
|
application.NewService,
|
||||||
|
NewHandler,
|
||||||
|
),
|
||||||
|
fx.Invoke(func(db *gorm.DB, log *zap.Logger) {
|
||||||
|
if err := auditinfra.Migrate(db); err != nil {
|
||||||
|
log.Fatal("audit migration failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
93
internal/modules/org/application/service.go
Normal file
93
internal/modules/org/application/service.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"mengstack/internal/kernel/errors"
|
||||||
|
"mengstack/internal/modules/org/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
repo domain.TenantRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo domain.TenantRepository) *Service {
|
||||||
|
return &Service{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateTenant(ctx context.Context, req domain.CreateTenantRequest) (domain.TenantDTO, error) {
|
||||||
|
if _, err := s.repo.FindBySlug(ctx, req.Slug); err == nil {
|
||||||
|
return domain.TenantDTO{}, errors.ErrConflict
|
||||||
|
}
|
||||||
|
|
||||||
|
tenant := &domain.Tenant{
|
||||||
|
Name: req.Name,
|
||||||
|
Slug: req.Slug,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.Create(ctx, tenant); err != nil {
|
||||||
|
return domain.TenantDTO{}, errors.Wrap(err, "CREATE_TENANT_FAILED", "failed to create tenant", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.ToTenantDTO(tenant), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListTenants(ctx context.Context) ([]domain.TenantDTO, error) {
|
||||||
|
tenants, err := s.repo.FindAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "QUERY_TENANTS_FAILED", "failed to query tenants", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
dtos := make([]domain.TenantDTO, len(tenants))
|
||||||
|
for i, t := range tenants {
|
||||||
|
dtos[i] = domain.ToTenantDTO(&t)
|
||||||
|
}
|
||||||
|
return dtos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetTenant(ctx context.Context, id string) (domain.TenantDTO, error) {
|
||||||
|
tenant, err := s.repo.FindByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return domain.TenantDTO{}, errors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.ToTenantDTO(tenant), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateTenant(ctx context.Context, id string, req domain.UpdateTenantRequest) (domain.TenantDTO, error) {
|
||||||
|
tenant, err := s.repo.FindByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return domain.TenantDTO{}, errors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != "" {
|
||||||
|
tenant.Name = req.Name
|
||||||
|
}
|
||||||
|
if req.Slug != "" {
|
||||||
|
if existing, err := s.repo.FindBySlug(ctx, req.Slug); err == nil && existing.ID != id {
|
||||||
|
return domain.TenantDTO{}, errors.ErrConflict
|
||||||
|
}
|
||||||
|
tenant.Slug = req.Slug
|
||||||
|
}
|
||||||
|
if req.Status != nil {
|
||||||
|
tenant.Status = *req.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.Update(ctx, tenant); err != nil {
|
||||||
|
return domain.TenantDTO{}, errors.Wrap(err, "UPDATE_TENANT_FAILED", "failed to update tenant", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.ToTenantDTO(tenant), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteTenant(ctx context.Context, id string) error {
|
||||||
|
if _, err := s.repo.FindByID(ctx, id); err != nil {
|
||||||
|
return errors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.Delete(ctx, id); err != nil {
|
||||||
|
return errors.Wrap(err, "DELETE_TENANT_FAILED", "failed to delete tenant", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
12
internal/modules/org/domain/repository.go
Normal file
12
internal/modules/org/domain/repository.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type TenantRepository interface {
|
||||||
|
Create(ctx context.Context, tenant *Tenant) error
|
||||||
|
FindByID(ctx context.Context, id string) (*Tenant, error)
|
||||||
|
FindBySlug(ctx context.Context, slug string) (*Tenant, error)
|
||||||
|
FindAll(ctx context.Context) ([]Tenant, error)
|
||||||
|
Update(ctx context.Context, tenant *Tenant) error
|
||||||
|
Delete(ctx context.Context, id string) error
|
||||||
|
}
|
||||||
48
internal/modules/org/domain/tenant.go
Normal file
48
internal/modules/org/domain/tenant.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Tenant struct {
|
||||||
|
ID string `json:"id" gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||||
|
Name string `json:"name" gorm:"size:128;not null"`
|
||||||
|
Slug string `json:"slug" gorm:"uniqueIndex;size:64;not null"`
|
||||||
|
Status int `json:"status" gorm:"default:1"`
|
||||||
|
Settings string `json:"settings" gorm:"type:text"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Tenant) TableName() string {
|
||||||
|
return "tenants"
|
||||||
|
}
|
||||||
|
|
||||||
|
type TenantDTO struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToTenantDTO(t *Tenant) TenantDTO {
|
||||||
|
return TenantDTO{
|
||||||
|
ID: t.ID,
|
||||||
|
Name: t.Name,
|
||||||
|
Slug: t.Slug,
|
||||||
|
Status: t.Status,
|
||||||
|
CreatedAt: t.CreatedAt,
|
||||||
|
UpdatedAt: t.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTenantRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Slug string `json:"slug" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateTenantRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Status *int `json:"status"`
|
||||||
|
}
|
||||||
13
internal/modules/org/infrastructure/migrate.go
Normal file
13
internal/modules/org/infrastructure/migrate.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package infrastructure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mengstack/internal/modules/org/domain"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Migrate(db *gorm.DB) error {
|
||||||
|
return db.AutoMigrate(
|
||||||
|
&domain.Tenant{},
|
||||||
|
)
|
||||||
|
}
|
||||||
47
internal/modules/org/infrastructure/tenant_repo.go
Normal file
47
internal/modules/org/infrastructure/tenant_repo.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package infrastructure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"mengstack/internal/modules/org/domain"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tenantRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTenantRepository(db *gorm.DB) domain.TenantRepository {
|
||||||
|
return &tenantRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *tenantRepo) Create(ctx context.Context, tenant *domain.Tenant) error {
|
||||||
|
return r.db.WithContext(ctx).Create(tenant).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *tenantRepo) FindByID(ctx context.Context, id string) (*domain.Tenant, error) {
|
||||||
|
var tenant domain.Tenant
|
||||||
|
err := r.db.WithContext(ctx).Where("id = ?", id).First(&tenant).Error
|
||||||
|
return &tenant, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *tenantRepo) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
||||||
|
var tenant domain.Tenant
|
||||||
|
err := r.db.WithContext(ctx).Where("slug = ?", slug).First(&tenant).Error
|
||||||
|
return &tenant, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *tenantRepo) FindAll(ctx context.Context) ([]domain.Tenant, error) {
|
||||||
|
var tenants []domain.Tenant
|
||||||
|
err := r.db.WithContext(ctx).Order("created_at DESC").Find(&tenants).Error
|
||||||
|
return tenants, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *tenantRepo) Update(ctx context.Context, tenant *domain.Tenant) error {
|
||||||
|
return r.db.WithContext(ctx).Save(tenant).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *tenantRepo) Delete(ctx context.Context, id string) error {
|
||||||
|
return r.db.WithContext(ctx).Where("id = ?", id).Delete(&domain.Tenant{}).Error
|
||||||
|
}
|
||||||
84
internal/modules/org/interfaces/handler.go
Normal file
84
internal/modules/org/interfaces/handler.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mengstack/internal/kernel/response"
|
||||||
|
"mengstack/internal/modules/org/application"
|
||||||
|
"mengstack/internal/modules/org/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) CreateTenant(c *gin.Context) {
|
||||||
|
var req domain.CreateTenantRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid request body"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dto, err := h.svc.CreateTenant(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListTenants(c *gin.Context) {
|
||||||
|
dtos, err := h.svc.ListTenants(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, dtos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetTenant(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
dto, err := h.svc.GetTenant(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UpdateTenant(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
var req domain.UpdateTenantRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid request body"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dto, err := h.svc.UpdateTenant(c.Request.Context(), id, req)
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) DeleteTenant(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
if err := h.svc.DeleteTenant(c.Request.Context(), id); err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, nil)
|
||||||
|
}
|
||||||
37
internal/modules/org/interfaces/routes.go
Normal file
37
internal/modules/org/interfaces/routes.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mengstack/internal/app/middleware"
|
||||||
|
"mengstack/internal/modules/org/application"
|
||||||
|
"mengstack/internal/modules/org/infrastructure"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupRoutes(r *gin.Engine, h *Handler, authMW gin.HandlerFunc) {
|
||||||
|
org := r.Group("/api/v1/org")
|
||||||
|
org.Use(authMW, middleware.MultiTenant())
|
||||||
|
{
|
||||||
|
org.POST("/tenants", h.CreateTenant)
|
||||||
|
org.GET("/tenants", h.ListTenants)
|
||||||
|
org.GET("/tenants/:id", h.GetTenant)
|
||||||
|
org.PUT("/tenants/:id", h.UpdateTenant)
|
||||||
|
org.DELETE("/tenants/:id", h.DeleteTenant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Module = fx.Module("org",
|
||||||
|
fx.Provide(
|
||||||
|
infrastructure.NewTenantRepository,
|
||||||
|
application.NewService,
|
||||||
|
NewHandler,
|
||||||
|
),
|
||||||
|
fx.Invoke(func(db *gorm.DB, log *zap.Logger) {
|
||||||
|
if err := infrastructure.Migrate(db); err != nil {
|
||||||
|
log.Fatal("org migration failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue
Block a user