feat(settings): M5 配置管理模块
DB-backed runtime settings with global/tenant scope, key-value store, type support (string/number/bool/json), tenant-level overrides with global fallback.
This commit is contained in:
parent
b13427e76c
commit
0eb6a96057
@ -16,6 +16,7 @@ import (
|
||||
rbacinterfaces "mengstack/internal/modules/rbac/interfaces"
|
||||
orginterfaces "mengstack/internal/modules/org/interfaces"
|
||||
auditinterfaces "mengstack/internal/modules/audit/interfaces"
|
||||
settingsinterfaces "mengstack/internal/modules/settings/interfaces"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
@ -43,6 +44,7 @@ func newEngine(
|
||||
rbacHandler *rbacinterfaces.Handler,
|
||||
orgHandler *orginterfaces.Handler,
|
||||
auditHandler *auditinterfaces.Handler,
|
||||
settingsHandler *settingsinterfaces.Handler,
|
||||
) *gin.Engine {
|
||||
ginMode := "release"
|
||||
if cfg.Server.Mode == "debug" || cfg.Server.Mode == "dev" {
|
||||
@ -65,6 +67,7 @@ func newEngine(
|
||||
rbacinterfaces.SetupRoutes(r, rbacHandler, authMW)
|
||||
orginterfaces.SetupRoutes(r, orgHandler, authMW)
|
||||
auditinterfaces.SetupRoutes(r, auditHandler, authMW)
|
||||
settingsinterfaces.SetupRoutes(r, settingsHandler, authMW)
|
||||
|
||||
return r
|
||||
}
|
||||
@ -109,6 +112,7 @@ func NewApp() *fx.App {
|
||||
rbacinterfaces.Module,
|
||||
orginterfaces.Module,
|
||||
auditinterfaces.Module,
|
||||
settingsinterfaces.Module,
|
||||
Module,
|
||||
)
|
||||
}
|
||||
|
||||
100
internal/modules/settings/application/service.go
Normal file
100
internal/modules/settings/application/service.go
Normal file
@ -0,0 +1,100 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"mengstack/internal/modules/settings/domain"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo domain.SettingRepository
|
||||
}
|
||||
|
||||
func NewService(repo domain.SettingRepository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *Service) GetGlobalSetting(ctx context.Context, key string) (*domain.SettingDTO, error) {
|
||||
setting, err := s.repo.FindByKey(ctx, "", key)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
dto := domain.ToSettingDTO(setting)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateGlobalSetting(ctx context.Context, key string, req *domain.UpdateSettingRequest, userID uint) error {
|
||||
setting := &domain.Setting{
|
||||
TenantID: "",
|
||||
Key: key,
|
||||
Value: req.Value,
|
||||
Type: req.Type,
|
||||
Scope: "global",
|
||||
UpdatedBy: userID,
|
||||
}
|
||||
if setting.Type == "" {
|
||||
setting.Type = "string"
|
||||
}
|
||||
return s.repo.Upsert(ctx, setting)
|
||||
}
|
||||
|
||||
func (s *Service) ListGlobalSettings(ctx context.Context) ([]domain.SettingDTO, error) {
|
||||
settings, err := s.repo.FindGlobal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]domain.SettingDTO, len(settings))
|
||||
for i, s := range settings {
|
||||
result[i] = domain.ToSettingDTO(&s)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetTenantSetting(ctx context.Context, tenantID, key string) (*domain.SettingDTO, error) {
|
||||
setting, err := s.repo.FindByKey(ctx, tenantID, key)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return s.GetGlobalSetting(ctx, key)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
dto := domain.ToSettingDTO(setting)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateTenantSetting(ctx context.Context, tenantID, key string, req *domain.UpdateSettingRequest, userID uint) error {
|
||||
setting := &domain.Setting{
|
||||
TenantID: tenantID,
|
||||
Key: key,
|
||||
Value: req.Value,
|
||||
Type: req.Type,
|
||||
Scope: "tenant",
|
||||
UpdatedBy: userID,
|
||||
}
|
||||
if setting.Type == "" {
|
||||
setting.Type = "string"
|
||||
}
|
||||
return s.repo.Upsert(ctx, setting)
|
||||
}
|
||||
|
||||
func (s *Service) ListTenantSettings(ctx context.Context, tenantID string) ([]domain.SettingDTO, error) {
|
||||
settings, err := s.repo.FindByTenant(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]domain.SettingDTO, len(settings))
|
||||
for i, s := range settings {
|
||||
result[i] = domain.ToSettingDTO(&s)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteTenantSetting(ctx context.Context, tenantID, key string) error {
|
||||
return s.repo.Delete(ctx, tenantID, key)
|
||||
}
|
||||
11
internal/modules/settings/domain/repository.go
Normal file
11
internal/modules/settings/domain/repository.go
Normal file
@ -0,0 +1,11 @@
|
||||
package domain
|
||||
|
||||
import "context"
|
||||
|
||||
type SettingRepository interface {
|
||||
Upsert(ctx context.Context, setting *Setting) error
|
||||
FindByKey(ctx context.Context, tenantID, key string) (*Setting, error)
|
||||
FindByTenant(ctx context.Context, tenantID string) ([]Setting, error)
|
||||
FindGlobal(ctx context.Context) ([]Setting, error)
|
||||
Delete(ctx context.Context, tenantID, key string) error
|
||||
}
|
||||
48
internal/modules/settings/domain/setting.go
Normal file
48
internal/modules/settings/domain/setting.go
Normal file
@ -0,0 +1,48 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type Setting struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
TenantID string `json:"tenant_id" gorm:"type:uuid;index:idx_tenant_key,unique;default:''"`
|
||||
Key string `json:"key" gorm:"size:128;not null;index:idx_tenant_key,unique"`
|
||||
Value string `json:"value" gorm:"type:text"`
|
||||
Type string `json:"type" gorm:"size:16;default:string"`
|
||||
Scope string `json:"scope" gorm:"size:16;default:tenant"`
|
||||
UpdatedBy uint `json:"updated_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (Setting) TableName() string {
|
||||
return "settings"
|
||||
}
|
||||
|
||||
type SettingDTO struct {
|
||||
ID uint `json:"id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
Scope string `json:"scope"`
|
||||
UpdatedBy uint `json:"updated_by"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func ToSettingDTO(s *Setting) SettingDTO {
|
||||
return SettingDTO{
|
||||
ID: s.ID,
|
||||
TenantID: s.TenantID,
|
||||
Key: s.Key,
|
||||
Value: s.Value,
|
||||
Type: s.Type,
|
||||
Scope: s.Scope,
|
||||
UpdatedBy: s.UpdatedBy,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateSettingRequest struct {
|
||||
Value string `json:"value" binding:"required"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
11
internal/modules/settings/infrastructure/migrate.go
Normal file
11
internal/modules/settings/infrastructure/migrate.go
Normal file
@ -0,0 +1,11 @@
|
||||
package infrastructure
|
||||
|
||||
import (
|
||||
"mengstack/internal/modules/settings/domain"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Migrate(db *gorm.DB) error {
|
||||
return db.AutoMigrate(&domain.Setting{})
|
||||
}
|
||||
72
internal/modules/settings/infrastructure/setting_repo.go
Normal file
72
internal/modules/settings/infrastructure/setting_repo.go
Normal file
@ -0,0 +1,72 @@
|
||||
package infrastructure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"mengstack/internal/modules/settings/domain"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type settingRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewSettingRepository(db *gorm.DB) domain.SettingRepository {
|
||||
return &settingRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *settingRepo) Upsert(ctx context.Context, setting *domain.Setting) error {
|
||||
var existing domain.Setting
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("tenant_id = ? AND key = ?", setting.TenantID, setting.Key).
|
||||
First(&existing).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return r.db.WithContext(ctx).Create(setting).Error
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&existing).
|
||||
Updates(map[string]interface{}{
|
||||
"value": setting.Value,
|
||||
"type": setting.Type,
|
||||
"updated_by": setting.UpdatedBy,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *settingRepo) FindByKey(ctx context.Context, tenantID, key string) (*domain.Setting, error) {
|
||||
var setting domain.Setting
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("tenant_id = ? AND key = ?", tenantID, key).
|
||||
First(&setting).Error
|
||||
return &setting, err
|
||||
}
|
||||
|
||||
func (r *settingRepo) FindByTenant(ctx context.Context, tenantID string) ([]domain.Setting, error) {
|
||||
var settings []domain.Setting
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("tenant_id = ?", tenantID).
|
||||
Order("key").
|
||||
Find(&settings).Error
|
||||
return settings, err
|
||||
}
|
||||
|
||||
func (r *settingRepo) FindGlobal(ctx context.Context) ([]domain.Setting, error) {
|
||||
var settings []domain.Setting
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("tenant_id = ''").
|
||||
Order("key").
|
||||
Find(&settings).Error
|
||||
return settings, err
|
||||
}
|
||||
|
||||
func (r *settingRepo) Delete(ctx context.Context, tenantID, key string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("tenant_id = ? AND key = ?", tenantID, key).
|
||||
Delete(&domain.Setting{}).Error
|
||||
}
|
||||
108
internal/modules/settings/interfaces/handler.go
Normal file
108
internal/modules/settings/interfaces/handler.go
Normal file
@ -0,0 +1,108 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"mengstack/internal/kernel/response"
|
||||
"mengstack/internal/modules/settings/application"
|
||||
"mengstack/internal/modules/settings/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) GetGlobalSetting(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
setting, err := h.svc.GetGlobalSetting(c.Request.Context(), key)
|
||||
if err != nil {
|
||||
response.HandleError(c, err)
|
||||
return
|
||||
}
|
||||
if setting == nil {
|
||||
response.Fail(c, response.NewBadRequest("setting not found"))
|
||||
return
|
||||
}
|
||||
response.Success(c, setting)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateGlobalSetting(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
var req domain.UpdateSettingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Fail(c, response.NewBadRequest("invalid request: "+err.Error()))
|
||||
return
|
||||
}
|
||||
userID := c.GetUint("user_id")
|
||||
if err := h.svc.UpdateGlobalSetting(c.Request.Context(), key, &req, userID); err != nil {
|
||||
response.HandleError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *Handler) ListGlobalSettings(c *gin.Context) {
|
||||
settings, err := h.svc.ListGlobalSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.HandleError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, settings)
|
||||
}
|
||||
|
||||
func (h *Handler) GetTenantSetting(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
tenantID := c.GetString("tenant_id")
|
||||
setting, err := h.svc.GetTenantSetting(c.Request.Context(), tenantID, key)
|
||||
if err != nil {
|
||||
response.HandleError(c, err)
|
||||
return
|
||||
}
|
||||
if setting == nil {
|
||||
response.Fail(c, response.NewBadRequest("setting not found"))
|
||||
return
|
||||
}
|
||||
response.Success(c, setting)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateTenantSetting(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
tenantID := c.GetString("tenant_id")
|
||||
var req domain.UpdateSettingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Fail(c, response.NewBadRequest("invalid request: "+err.Error()))
|
||||
return
|
||||
}
|
||||
userID := c.GetUint("user_id")
|
||||
if err := h.svc.UpdateTenantSetting(c.Request.Context(), tenantID, key, &req, userID); err != nil {
|
||||
response.HandleError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *Handler) ListTenantSettings(c *gin.Context) {
|
||||
tenantID := c.GetString("tenant_id")
|
||||
settings, err := h.svc.ListTenantSettings(c.Request.Context(), tenantID)
|
||||
if err != nil {
|
||||
response.HandleError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, settings)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteTenantSetting(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
tenantID := c.GetString("tenant_id")
|
||||
if err := h.svc.DeleteTenantSetting(c.Request.Context(), tenantID, key); err != nil {
|
||||
response.HandleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "deleted"})
|
||||
}
|
||||
44
internal/modules/settings/interfaces/routes.go
Normal file
44
internal/modules/settings/interfaces/routes.go
Normal file
@ -0,0 +1,44 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"mengstack/internal/app/middleware"
|
||||
"mengstack/internal/modules/settings/application"
|
||||
"mengstack/internal/modules/settings/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,
|
||||
) {
|
||||
settings := r.Group("/api/v1/settings")
|
||||
settings.Use(authMW, middleware.MultiTenant())
|
||||
{
|
||||
settings.GET("", h.ListGlobalSettings)
|
||||
settings.GET("/global/:key", h.GetGlobalSetting)
|
||||
settings.PUT("/global/:key", h.UpdateGlobalSetting)
|
||||
|
||||
settings.GET("/tenant", h.ListTenantSettings)
|
||||
settings.GET("/tenant/:key", h.GetTenantSetting)
|
||||
settings.PUT("/tenant/:key", h.UpdateTenantSetting)
|
||||
settings.DELETE("/tenant/:key", h.DeleteTenantSetting)
|
||||
}
|
||||
}
|
||||
|
||||
var Module = fx.Module("settings",
|
||||
fx.Provide(
|
||||
infrastructure.NewSettingRepository,
|
||||
application.NewService,
|
||||
NewHandler,
|
||||
),
|
||||
fx.Invoke(func(db *gorm.DB, log *zap.Logger) {
|
||||
if err := infrastructure.Migrate(db); err != nil {
|
||||
log.Fatal("settings migration failed", zap.Error(err))
|
||||
}
|
||||
}),
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user