feat(gateway): 多成员租户·成员管理后端 —— 租户目录 + 成员增改删
真·多租户地基(此前只有单人默认租户 + admin 页是 mock): - store:ListTenants(含成员数+余额) / MyTenants / ListMembers(join user) / AddMemberByEmail(邮箱归一化小写、幂等 upsert、复活已移除、未注册报友好错) / SetMemberRole(禁改 owner、禁直接授 owner) / RemoveMember(软移除、禁移除 owner) / ValidRole。 - admin 接口:GET/POST /admin/tenants、GET/POST /admin/tenants/:id/members、 PUT/DELETE /admin/tenants/:id/members/:uid。 live 验证:加 demoB 入公司A→member、列表、改角色→admin、移除 owner 被拒、移除 member,全过。 admin 前端接线 + 租户切换 见后续。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -286,6 +286,99 @@ func (h *Handler) SaveBillingConfig(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
// AdminTenants: GET /api/v1/admin/tenants —— 全平台租户目录(含成员数 + 余额)。
|
||||
func (h *Handler) AdminTenants(c *gin.Context) {
|
||||
rows, err := h.db.ListTenants(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"tenants": rows})
|
||||
}
|
||||
|
||||
// AdminCreateTenant: POST /api/v1/admin/tenants {name, slug, owner_email?} —— 新建租户(可选指定 owner)。
|
||||
func (h *Handler) AdminCreateTenant(c *gin.Context) {
|
||||
var b struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Plan string `json:"plan"`
|
||||
OwnerEmail string `json:"owner_email"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&b); err != nil || strings.TrimSpace(b.Name) == "" || strings.TrimSpace(b.Slug) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name 和 slug 必填"})
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
t, err := h.db.CreateTenant(ctx, b.Name, b.Slug, b.Plan)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if em := strings.TrimSpace(b.OwnerEmail); em != "" {
|
||||
if _, err := h.db.AddMemberByEmail(ctx, t.ID, em, store.RoleOwner); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"tenant": t, "warn": "租户已建,但指定 owner 失败:" + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"tenant": t})
|
||||
}
|
||||
|
||||
// AdminMembers: GET /api/v1/admin/tenants/:id/members —— 某租户成员列表。
|
||||
func (h *Handler) AdminMembers(c *gin.Context) {
|
||||
rows, err := h.db.ListMembers(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"members": rows})
|
||||
}
|
||||
|
||||
// AdminAddMember: POST /api/v1/admin/tenants/:id/members {email, role} —— 按邮箱加成员。
|
||||
func (h *Handler) AdminAddMember(c *gin.Context) {
|
||||
var b struct {
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&b); err != nil || strings.TrimSpace(b.Email) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "email 必填"})
|
||||
return
|
||||
}
|
||||
if b.Role == "" {
|
||||
b.Role = store.RoleMember
|
||||
}
|
||||
m, err := h.db.AddMemberByEmail(c.Request.Context(), c.Param("id"), strings.TrimSpace(b.Email), b.Role)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"member": m})
|
||||
}
|
||||
|
||||
// AdminSetMemberRole: PUT /api/v1/admin/tenants/:id/members/:uid {role} —— 改成员角色。
|
||||
func (h *Handler) AdminSetMemberRole(c *gin.Context) {
|
||||
var b struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&b); err != nil || b.Role == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "role 必填"})
|
||||
return
|
||||
}
|
||||
if err := h.db.SetMemberRole(c.Request.Context(), c.Param("id"), c.Param("uid"), b.Role); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
// AdminRemoveMember: DELETE /api/v1/admin/tenants/:id/members/:uid —— 软移除成员。
|
||||
func (h *Handler) AdminRemoveMember(c *gin.Context) {
|
||||
if err := h.db.RemoveMember(c.Request.Context(), c.Param("id"), c.Param("uid")); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
// GrantCredits: POST /api/v1/admin/credits/grant —— 给租户充值/发放积分(记账本 + 增余额)。
|
||||
func (h *Handler) GrantCredits(c *gin.Context) {
|
||||
var b struct {
|
||||
|
||||
Reference in New Issue
Block a user