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
@@ -0,0 +1,28 @@
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/sundynix/sundynix-gateway/internal/store"
)
// CtxTenantID 是鉴权后写入 gin.Context 的当前租户 ID 键。
const CtxTenantID = "tenant_id"
// TenantContext 解析当前用户的默认租户并注入 tenant_id(多租户作用域的事实源)。
// 须挂在 Auth 之后(依赖已注入的 uid);未登录请求跳过。存量/异常无租户者由 EnsureDefaultTenant
// 幂等兜底补建,保证任何已登录请求都能拿到 tenant_id。
//
// 注:当前每请求解析一次(1–2 条按索引查询)。后续可把 tenant_id 嵌入 JWT / 加缓存去掉此开销。
func TenantContext(db *store.Postgres) gin.HandlerFunc {
return func(c *gin.Context) {
if v, ok := c.Get(CtxUserID); ok {
if uid, _ := v.(string); uid != "" {
if t, err := db.EnsureDefaultTenant(c.Request.Context(), uid, ""); err == nil && t != nil {
c.Set(CtxTenantID, t.ID)
}
}
}
c.Next()
}
}