feat(gateway): 多租户地基·增量1 —— 租户身份/成员/上下文(T4.A/SaaS P1)

按 SAAS_DESIGN.md P1 第一刀(真·多租户 + 桌面端为主):先立租户身份,不动查询。
- store.Tenant / TenantMember 表 + AutoMigrate
- store: CreateTenant / AddMember(幂等) / DefaultTenantForUser / EnsureDefaultTenant(幂等) /
  BackfillDefaultTenants / GetTenant / MemberRole
- 注册即建单人默认租户(owner);启动回填给存量用户补建(幂等)
- middleware.TenantContext(挂 Auth 后)解析当前租户→注入 tenant_id;handler.tenantID(c) 助手
- GET /api/v1/tenants/current 验证端点(租户上下文 + 角色)
- live:存量用户(回填 7 租户)/tenants/current 返回默认租户+owner;新注册自动建租户

下一步(增量2):核心表加 tenant_id + 统一 gorm scope 强制隔离 + 存量行回填 + 重写查询。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-06 17:31:09 +08:00
parent 2f19e322e3
commit 693a8f09e9
8 changed files with 250 additions and 4 deletions
+12
View File
@@ -2,6 +2,7 @@ package handler
import (
"errors"
"log"
"net/http"
"strings"
@@ -11,6 +12,13 @@ import (
"github.com/sundynix/sundynix-gateway/internal/store"
)
func firstNonEmpty(a, b string) string {
if a != "" {
return a
}
return b
}
// userJSON 是对外的用户视图(绝不含密码哈希)。
func userJSON(u *store.User) gin.H {
return gin.H{"id": u.ID, "email": u.Email, "name": u.Name}
@@ -46,6 +54,10 @@ func (h *Handler) Register(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}
// 多租户:每个新用户建一个单人默认租户(owner)。失败不阻断注册(中间件会兜底补建)。
if _, e := h.db.EnsureDefaultTenant(c.Request.Context(), u.ID, firstNonEmpty(strings.TrimSpace(body.Name), email)); e != nil {
log.Printf("[auth] 建默认租户失败 uid=%s: %v", u.ID, e)
}
issueToken(c, u)
}