package store import ( "context" "errors" "strings" "time" "gorm.io/gorm" "gorm.io/gorm/clause" ) // ValidRole 校验角色取值合法。 func ValidRole(role string) bool { switch role { case RoleOwner, RoleAdmin, RoleMember, RoleViewer, RoleBillingAdmin: return true } return false } // 角色/成员状态常量。 const ( RoleOwner = "owner" RoleAdmin = "admin" RoleMember = "member" RoleViewer = "viewer" RoleBillingAdmin = "billing_admin" ) // RoleRank 返回角色在「操作/管理」阶梯上的等级(越大权越高),供 RequireTenantRole 门控。 // 语义:viewer 与 billing_admin 均为只读(1)——billing_admin 是「财务只读」角色,管账不跑任务, // 故不能提交(烧租户积分);member 可操作(2);admin(3)/owner(4) 递增管理权。 // 未知角色 / 非成员(空串)记 0,一律拦下。当前唯一接入点:SubmitTask 要求 ≥ member(挡 viewer/billing_admin 烧租户积分)。 func RoleRank(role string) int { switch role { case RoleOwner: return 4 case RoleAdmin: return 3 case RoleMember: return 2 case RoleViewer, RoleBillingAdmin: return 1 } return 0 } // CreateTenant 建一个租户。 func (p *Postgres) CreateTenant(ctx context.Context, name, slug, plan string) (*Tenant, error) { if p.db == nil { return nil, errStoreDisabled } if plan == "" { plan = "free" } t := &Tenant{Name: name, Slug: slug, Plan: plan, Status: "active"} if err := p.db.WithContext(ctx).Create(t).Error; err != nil { return nil, err } return t, nil } // AddMember 添加成员(幂等:已存在则不重复插入)。 func (p *Postgres) AddMember(ctx context.Context, tenantID, userID, role string) error { if p.db == nil { return errStoreDisabled } m := &TenantMember{TenantID: tenantID, UserID: userID, Role: role, Status: "active"} // 命中唯一约束(tenant_id,user_id)则忽略,保证幂等。 return p.db.WithContext(ctx). Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "tenant_id"}, {Name: "user_id"}}, DoNothing: true}). Create(m).Error } // DefaultTenantForUser 返回用户所属的默认租户(当前取其最早加入的 active 成员关系;无则 nil)。 // tenantIDForOwner 返回某 user 的默认(最早 active)租户 id;无则空串。 // 供异步路径(入库 / 评测回写,无请求 ctx 租户)在写入时补 tenant_id。 func (p *Postgres) tenantIDForOwner(ctx context.Context, owner string) string { if p.db == nil || owner == "" { return "" } var m TenantMember if err := p.db.WithContext(ctx).Select("tenant_id"). Where("user_id = ? AND status = ?", owner, "active"). Order("created_at asc").First(&m).Error; err != nil { return "" } return m.TenantID } func (p *Postgres) DefaultTenantForUser(ctx context.Context, userID string) (*Tenant, error) { if p.db == nil { return nil, errStoreDisabled } var m TenantMember err := p.db.WithContext(ctx). Where("user_id = ? AND status = ?", userID, "active"). Order("created_at asc").First(&m).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } return nil, err } var t Tenant if err := p.db.WithContext(ctx).First(&t, "id = ?", m.TenantID).Error; err != nil { return nil, err } return &t, nil } // EnsureDefaultTenant 幂等地保证用户有一个默认租户:已有 active 成员关系则返回其默认租户; // 否则建一个单人租户(owner) 并返回。用于注册与存量回填。 func (p *Postgres) EnsureDefaultTenant(ctx context.Context, userID, name string) (*Tenant, error) { if p.db == nil { return nil, errStoreDisabled } if t, err := p.DefaultTenantForUser(ctx, userID); err != nil { return nil, err } else if t != nil { return t, nil } if name == "" { name = "个人工作区" } t, err := p.CreateTenant(ctx, name, "default-"+userID, "free") if err != nil { return nil, err } if err := p.AddMember(ctx, t.ID, userID, RoleOwner); err != nil { return nil, err } return t, nil } // BackfillDefaultTenants 给所有还没有租户成员关系的存量用户补建默认租户(启动时幂等调用)。 func (p *Postgres) BackfillDefaultTenants(ctx context.Context) (int, error) { if p.db == nil { return 0, errStoreDisabled } var users []User // 无 active 成员关系的用户。 if err := p.db.WithContext(ctx). Where("id NOT IN (?)", p.db.Model(&TenantMember{}).Select("user_id").Where("status = ?", "active")). Find(&users).Error; err != nil { return 0, err } n := 0 for _, u := range users { if _, err := p.EnsureDefaultTenant(ctx, u.ID, firstNonEmpty(u.Name, u.Email)); err != nil { return n, err } n++ } return n, nil } // GetTenant 按 id 取租户(不存在返回 nil)。 func (p *Postgres) GetTenant(ctx context.Context, id string) (*Tenant, error) { if p.db == nil { return nil, errStoreDisabled } var t Tenant if err := p.db.WithContext(ctx).First(&t, "id = ?", id).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } return nil, err } return &t, nil } // MemberRole 返回用户在某租户的角色(无成员关系返回空)。 func (p *Postgres) MemberRole(ctx context.Context, tenantID, userID string) string { if p.db == nil { return "" } var m TenantMember if err := p.db.WithContext(ctx). Where("tenant_id = ? AND user_id = ? AND status = ?", tenantID, userID, "active"). First(&m).Error; err != nil { return "" } return m.Role } func firstNonEmpty(a, b string) string { if a != "" { return a } return b } // ---- 成员管理(多成员租户)---- // MemberInfo 是一条成员视图(join user 拿邮箱/名字)。 type MemberInfo struct { UserID string `json:"user_id"` Email string `json:"email"` Name string `json:"name"` Role string `json:"role"` Status string `json:"status"` JoinedAt time.Time `json:"joined_at"` } // ListMembers 返回某租户的成员(含邮箱/名字/角色),排除已移除。 func (p *Postgres) ListMembers(ctx context.Context, tenantID string) ([]MemberInfo, error) { if p.db == nil { return nil, nil } var out []MemberInfo err := p.db.WithContext(ctx).Table("sundynix_tenant_member m"). Select("m.user_id, u.email, u.name, m.role, m.status, m.created_at as joined_at"). Joins("JOIN sundynix_user u ON u.id = m.user_id"). Where("m.tenant_id = ? AND m.status <> ?", tenantID, "removed"). Order("m.created_at asc").Scan(&out).Error return out, err } // AddMemberByEmail 按邮箱把已注册用户加入租户(幂等:已在则改其角色并置 active,含"复活"已移除者)。 // 用户不存在返回错误(本阶段不做外发邀请邮件,须对方先注册)。 func (p *Postgres) AddMemberByEmail(ctx context.Context, tenantID, email, role string) (*MemberInfo, error) { if p.db == nil { return nil, errStoreDisabled } if !ValidRole(role) { return nil, errors.New("非法角色") } // 注册时邮箱按小写存,这里也归一化,避免大小写导致查不到。 u, err := p.GetUserByEmail(ctx, strings.ToLower(strings.TrimSpace(email))) if errors.Is(err, gorm.ErrRecordNotFound) || u == nil { return nil, errors.New("该邮箱尚未注册,请对方先注册后再加入") } if err != nil { return nil, err } if err := p.db.WithContext(ctx).Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "tenant_id"}, {Name: "user_id"}}, DoUpdates: clause.Assignments(map[string]any{"role": role, "status": "active", "updated_at": time.Now()}), }).Create(&TenantMember{TenantID: tenantID, UserID: u.ID, Role: role, Status: "active"}).Error; err != nil { return nil, err } // 若该租户已启用全员空间,新成员自动纳入(全员空间的语义:全租户成员皆在内)。 p.autoJoinTenantSpace(ctx, tenantID, u.ID, role) return &MemberInfo{UserID: u.ID, Email: u.Email, Name: u.Name, Role: role, Status: "active"}, nil } // SetMemberRole 改成员角色。不允许把 owner 降级(须先转让 owner,超出本阶段)。 func (p *Postgres) SetMemberRole(ctx context.Context, tenantID, userID, role string) error { if p.db == nil { return errStoreDisabled } if !ValidRole(role) { return errors.New("非法角色") } if p.MemberRole(ctx, tenantID, userID) == RoleOwner { return errors.New("不能改 owner 的角色") } if role == RoleOwner { return errors.New("不能直接授予 owner(须转让)") } return p.db.WithContext(ctx).Model(&TenantMember{}). Where("tenant_id = ? AND user_id = ?", tenantID, userID).Update("role", role).Error } // RemoveMember 软移除成员(status=removed)。不能移除 owner。 func (p *Postgres) RemoveMember(ctx context.Context, tenantID, userID string) error { if p.db == nil { return errStoreDisabled } if p.MemberRole(ctx, tenantID, userID) == RoleOwner { return errors.New("不能移除 owner") } return p.db.WithContext(ctx).Model(&TenantMember{}). Where("tenant_id = ? AND user_id = ?", tenantID, userID).Update("status", "removed").Error } // TenantInfo 是管理端租户目录一行(含成员数 + 余额)。 type TenantInfo struct { ID string `json:"id"` Name string `json:"name"` Slug string `json:"slug"` Plan string `json:"plan"` Status string `json:"status"` CreditBalanceMicro int64 `json:"credit_balance_micro"` SharedBilling bool `json:"shared_billing"` Members int64 `json:"members"` } // ListTenants 返回全部租户(管理端目录:含 active 成员数 + 积分余额)。 func (p *Postgres) ListTenants(ctx context.Context) ([]TenantInfo, error) { if p.db == nil { return nil, nil } var out []TenantInfo err := p.db.WithContext(ctx).Table("sundynix_tenant t"). Select("t.id, t.name, t.slug, t.plan, t.status, t.credit_balance_micro, t.shared_billing, "+ "(SELECT count(*) FROM sundynix_tenant_member m WHERE m.tenant_id = t.id AND m.status = 'active') as members"). Where("t.deleted_at IS NULL").Order("t.created_at asc").Scan(&out).Error return out, err } // MyTenants 返回某用户所属(active)的全部租户(供多租户切换)。 func (p *Postgres) MyTenants(ctx context.Context, userID string) ([]TenantInfo, error) { if p.db == nil { return nil, nil } var out []TenantInfo err := p.db.WithContext(ctx).Table("sundynix_tenant t"). Select("t.id, t.name, t.slug, t.plan, t.status, t.credit_balance_micro, t.shared_billing, "+ "(SELECT count(*) FROM sundynix_tenant_member m2 WHERE m2.tenant_id = t.id AND m2.status = 'active') as members"). Joins("JOIN sundynix_tenant_member m ON m.tenant_id = t.id"). Where("m.user_id = ? AND m.status = 'active' AND t.deleted_at IS NULL", userID). Order("t.created_at asc").Scan(&out).Error return out, err } // ---- 租户切换 + 共享计费(增量2)---- // PersonalTenantForUser 返回用户自己的个人租户(最早加入且角色 owner 的租户 = 注册默认租户)。 func (p *Postgres) PersonalTenantForUser(ctx context.Context, userID string) string { if p.db == nil { return "" } var m TenantMember if err := p.db.WithContext(ctx).Select("tenant_id"). Where("user_id = ? AND status = 'active' AND role = ?", userID, RoleOwner). Order("created_at asc").First(&m).Error; err != nil { return "" } return m.TenantID } // ActiveTenantForUser 解析用户当前活跃租户:user.active_tenant_id 若已设且仍是 active 成员 → 用之; // 否则回退默认租户(并保证默认租户存在)。中间件每请求调用。 func (p *Postgres) ActiveTenantForUser(ctx context.Context, userID string) (*Tenant, error) { def, err := p.EnsureDefaultTenant(ctx, userID, "") if err != nil { return nil, err } var u User if e := p.db.WithContext(ctx).Select("active_tenant_id").First(&u, "id = ?", userID).Error; e == nil { if u.ActiveTenantID != "" && u.ActiveTenantID != def.ID && p.MemberRole(ctx, u.ActiveTenantID, userID) != "" { if t, _ := p.GetTenant(ctx, u.ActiveTenantID); t != nil { return t, nil } } } return def, nil } // SetActiveTenant 切换用户活跃租户(须为其 active 成员)。 func (p *Postgres) SetActiveTenant(ctx context.Context, userID, tenantID string) error { if p.db == nil { return errStoreDisabled } if p.MemberRole(ctx, tenantID, userID) == "" { return errors.New("你不是该租户的成员") } return p.db.WithContext(ctx).Model(&User{}).Where("id = ?", userID).Update("active_tenant_id", tenantID).Error } // SetSharedBilling 设某租户共享计费开关。 func (p *Postgres) SetSharedBilling(ctx context.Context, tenantID string, on bool) error { if p.db == nil { return errStoreDisabled } return p.db.WithContext(ctx).Model(&Tenant{}).Where("id = ?", tenantID).Update("shared_billing", on).Error } // ResolveBillingTenantID 决定"这次消耗记谁的池子":本人是 active 租户 owner,或 active 开了 // shared_billing → 记 active 租户;否则记本人个人租户(各付各的)。 func (p *Postgres) ResolveBillingTenantID(ctx context.Context, userID, activeTenantID string) string { if activeTenantID == "" { return "" } if p.MemberRole(ctx, activeTenantID, userID) == RoleOwner { return activeTenantID } if t, _ := p.GetTenant(ctx, activeTenantID); t != nil && t.SharedBilling { return activeTenantID } if personal := p.PersonalTenantForUser(ctx, userID); personal != "" { return personal } return activeTenantID } // SetTenantPlan 更改租户方案等级。 func (p *Postgres) SetTenantPlan(ctx context.Context, tenantID string, plan string) error { if p.db == nil { return errStoreDisabled } return p.db.WithContext(ctx).Model(&Tenant{}).Where("id = ?", tenantID).Update("plan", plan).Error } // SetTenantStatus 更改租户状态(active / suspended)。 func (p *Postgres) SetTenantStatus(ctx context.Context, tenantID string, status string) error { if p.db == nil { return errStoreDisabled } return p.db.WithContext(ctx).Model(&Tenant{}).Where("id = ?", tenantID).Update("status", status).Error }