feat(gateway): 多租户增量2 —— 租户切换 + 每租户共享计费开关

用户可切活跃租户,消耗按"计费租户"扣(数据仍落活跃租户/工作区):
- User.active_tenant_id(切换持久化)+ Tenant.shared_billing(每租户开关,默认关)。
- 中间件 ActiveTenantForUser:active_tenant_id 若有效(仍是成员)则用之,否则默认租户。
- 计费目标解析 ResolveBillingTenantID:本人是活跃租户 owner 或该租户 shared_billing 开
  → 记活跃租户;否则记本人个人租户(各付各的)。SubmitTask 的硬拦截 + 用量都按它走。
- 接口:GET /me/tenants、POST /me/tenant(切换)、PUT /admin/tenants/:id/shared-billing;
  /tenants/current 改显"可花余额"(计费租户) + billing_shared 标记。

live 验证(demoB 是公司A 成员、个人租户=公司B):切到公司A 后——
shared OFF:任务落公司A 工作区、用量扣公司B(个人),公司A 不变;
shared ON:用量扣公司A(共享),公司B 不变;/tenants/current 余额随之在 6.48↔15.92 切换。全过。

前端切换器 + admin 开关见后续。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-08 10:52:17 +08:00
parent aa10037511
commit b98cf3c718
6 changed files with 144 additions and 17 deletions
@@ -323,6 +323,22 @@ func (h *Handler) AdminCreateTenant(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"tenant": t})
}
// AdminSetSharedBilling: PUT /api/v1/admin/tenants/:id/shared-billing {shared_billing} —— 设共享计费开关。
func (h *Handler) AdminSetSharedBilling(c *gin.Context) {
var b struct {
SharedBilling bool `json:"shared_billing"`
}
if err := c.ShouldBindJSON(&b); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad body"})
return
}
if err := h.db.SetSharedBilling(c.Request.Context(), c.Param("id"), b.SharedBilling); err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
// 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"))