ProductReadyProductReady

组织与团队管理

工作区(Space)管理、成员邀请、角色权限、团队协作完整指南

组织与团队管理

ProductReady 使用 Better Auth 组织插件实现了完整的多租户协作功能。本指南涵盖工作区管理、成员邀请、角色权限、团队协作等核心功能。

术语说明: 在 ProductReady 中,"工作区"(Space)和"组织"(Organization)是同一概念的不同表述。代码中使用 Organization,用户界面中使用 Space。


快速概览

核心功能

  • ✅ 创建和管理多个工作区
  • ✅ 邀请成员加入工作区(邮箱邀请 或 可分享邀请链接)
  • ✅ 链接加入可选管理员审批
  • ✅ 基于角色的访问控制(RBAC)
  • ✅ 团队/群组管理
  • ✅ 工作区设置和计费管理
  • ✅ 离开或删除工作区

角色体系

  • Owner(所有者): 拥有全部权限,包括删除工作区
  • Admin(管理员): 几乎全部权限,不能删除工作区或修改计费
  • Member(成员): 基本工作权限,可以创建和编辑内容
  • Viewer(查看者): 只读权限,可以查看但不能修改

工作区(Space)管理

创建工作区

用户登录后可以创建多个工作区,用于不同的项目或团队。

创建工作区的方式

  1. 点击左侧边栏顶部的工作区选择器
  2. 点击 "+ 新建工作区" 按钮
  3. 输入工作区名称和描述
  4. 点击 "创建"
'use client';

import { authClient } from '~/lib/auth/client';
import { useState } from 'react';

export function CreateSpaceButton() {
  const [name, setName] = useState('');
  const [slug, setSlug] = useState('');

  async function handleCreate() {
    const result = await authClient.organization.create({
      name,
      slug, // 可选,用于 URL
    });

    if (result.error) {
      console.error('创建失败:', result.error);
      return;
    }

    console.log('工作区已创建:', result.data);
  }

  return (
    <div>
      <input
        type="text"
        placeholder="工作区名称"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <button onClick={handleCreate}>创建工作区</button>
    </div>
  );
}

切换工作区

用户可以在多个工作区之间快速切换。

切换方式

  1. 点击左侧边栏顶部的工作区选择器
  2. 从列表中选择目标工作区
  3. 页面自动切换到新工作区的上下文
'use client';

import { authClient } from '~/lib/auth/client';

export function SwitchSpace({ organizationId }: { organizationId: string }) {
  async function handleSwitch() {
    await authClient.organization.setActive({
      organizationId,
    });
    
    // 刷新页面以更新上下文
    window.location.reload();
  }

  return (
    <button onClick={handleSwitch}>
      切换到此工作区
    </button>
  );
}

更新工作区信息

工作区所有者和管理员可以更新工作区的基本信息。

'use client';

import { authClient } from '~/lib/auth/client';

export function UpdateSpace() {
  async function handleUpdate(organizationId: string, updates: { name?: string; slug?: string }) {
    const result = await authClient.organization.update({
      organizationId,
      data: updates,
    });

    if (result.error) {
      console.error('更新失败:', result.error);
      return;
    }

    console.log('工作区已更新');
  }

  return (
    <button onClick={() => handleUpdate('org-id', { name: '新名称' })}>
      更新工作区
    </button>
  );
}

删除工作区

只有工作区所有者才能删除工作区。删除是不可逆的操作。

删除步骤

  1. 打开工作区设置(点击侧边栏底部的"设置"按钮)
  2. 切换到"常规"标签页
  3. 滚动到"危险区域"
  4. 点击 "删除工作区" 按钮
  5. 输入工作区名称确认
  6. 点击 "确认删除"

警告: 删除工作区会永久删除所有相关数据,包括成员、团队、项目、文档等。此操作不可撤销!

'use client';

import { authClient } from '~/lib/auth/client';

export function DeleteSpace({ organizationId }: { organizationId: string }) {
  async function handleDelete() {
    const confirmed = window.confirm('确定要删除此工作区吗?所有数据将被永久删除!');
    if (!confirmed) return;

    const result = await authClient.organization.delete({
      organizationId,
    });

    if (result.error) {
      console.error('删除失败:', result.error);
      return;
    }

    // 删除成功,跳转到工作区列表
    window.location.href = '/spaces';
  }

  return (
    <button onClick={handleDelete} className="btn-danger">
      删除工作区
    </button>
  );
}

成员管理

邀请成员

管理员和所有者可以通过邮箱邀请新成员加入工作区。

邀请步骤

  1. 打开工作区设置 → "成员" 标签页
  2. 点击 "邀请成员" 按钮
  3. 输入成员的邮箱地址
  4. 选择角色(Member、Admin、Viewer)
  5. 点击 "发送邀请"

被邀请者会收到邀请邮件,点击链接即可接受邀请。

'use client';

import { authClient } from '~/lib/auth/client';
import { useState } from 'react';

export function InviteMemberForm() {
  const [email, setEmail] = useState('');
  const [role, setRole] = useState<'member' | 'admin'>('member');

  async function handleInvite() {
    const result = await authClient.organization.inviteMember({
      email,
      role,
    });

    if (result.error) {
      console.error('邀请失败:', result.error);
      return;
    }

    alert('邀请已发送!');
    setEmail('');
  }

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleInvite(); }}>
      <input
        type="email"
        placeholder="成员邮箱"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required
      />
      <select value={role} onChange={(e) => setRole(e.target.value as any)}>
        <option value="member">成员</option>
        <option value="admin">管理员</option>
      </select>
      <button type="submit">发送邀请</button>
    </form>
  );
}

通过邀请链接邀请

和邮箱邀请(一次性、绑定单个邮箱)不同,邀请链接可以重复使用——任何拿到链接的人登录后即可加入。一个空间可以同时存在多条链接,各自独立设置角色、有效期和使用次数上限。这是 Slack、Discord、Notion 等主流协作工具的标配做法。

管理员和所有者可以生成一条可分享的邀请链接,不需要提前知道对方邮箱即可让其直接加入工作区。

创建步骤

  1. 打开工作区设置 → "成员" 标签页
  2. 点击 "邀请成员""邀请链接" 标签
  3. 选择角色(Admin、Member 或 Viewer,不能选 Owner)、有效期(7 天 / 30 天 / 永不过期),可选设置使用次数上限
  4. 可选开启 "需要审批",开启后对方点击链接加入会进入待审批队列,而不是立即成为成员
  5. 点击 "创建邀请链接" 并复制链接分享出去

已有链接会排在最前面(复制是最常用的操作);创建新链接收进 "+ 新建链接" 按钮后面。链接可随时撤销——撤销后任何人打开都会看到"链接已失效"。

'use client';

import { orpc } from '~/lib/orpc/provider';
import { useMutation, useQuery } from '@tanstack/react-query';

export function InviteLinkExample() {
  const { data: links } = useQuery(orpc.inviteLinks.list.queryOptions());

  const createLink = useMutation(orpc.inviteLinks.create.mutationOptions());
  const revokeLink = useMutation(orpc.inviteLinks.revoke.mutationOptions());

  function handleCreate() {
    createLink.mutate({
      role: 'member',
      requiresApproval: false,
      expiresAt: null, // 永不过期
      maxUses: null,   // 不限次数
    });
  }

  return (
    <div>
      <button onClick={handleCreate}>创建邀请链接</button>
      {links?.map((link) => (
        <div key={link.id}>
          <code>{`${location.origin}/invite/join/${link.id}`}</code>
          <button onClick={() => revokeLink.mutate({ inviteLinkId: link.id })}>撤销</button>
        </div>
      ))}
    </div>
  );
}

加入是在公开的 /invite/join/[token] 页面上一次明确的点击——未登录的访问者会先跳去注册,注册完自动跳回来继续加入流程。座位配额的判断和邮箱邀请一致:如果套餐的成员上限已满,加入者会看到升级提示,而不是被静默加入。


管理加入请求

当某条邀请链接开启了 "需要审批",点击加入不会立即成为成员,而是生成一条待审批的加入请求。管理员在同一个 "成员" 标签页里处理。

审批步骤

  1. 打开工作区设置 → "成员" 标签页
  2. 待处理的请求会显示在成员列表上方的 "加入请求" 区块,带数量徽章
  3. 点击 "批准" 将其加入为成员(同样会检查座位配额),或点击 "拒绝" 驳回

无论批准还是拒绝,申请人都会收到一条站内通知。


查看待处理邀请

管理员可以查看和管理所有待处理的邀请。

查看步骤

  1. 打开工作区设置 → "成员" 标签页
  2. 在成员列表下方可以看到"待处理邀请"部分
  3. 可以取消任何待处理的邀请
'use client';

import { authClient } from '~/lib/auth/client';
import { useEffect, useState } from 'react';

interface Invitation {
  id: string;
  email: string;
  role: string;
  status: string;
  expiresAt: Date;
}

export function PendingInvitations() {
  const [invitations, setInvitations] = useState<Invitation[]>([]);

  useEffect(() => {
    loadInvitations();
  }, []);

  async function loadInvitations() {
    const result = await authClient.organization.listInvitations();
    if (result.data) {
      setInvitations(result.data as Invitation[]);
    }
  }

  async function cancelInvitation(invitationId: string) {
    await authClient.organization.cancelInvitation({
      invitationId,
    });
    loadInvitations(); // 刷新列表
  }

  return (
    <div>
      <h3>待处理邀请</h3>
      {invitations.map((inv) => (
        <div key={inv.id}>
          <span>{inv.email} - {inv.role}</span>
          <button onClick={() => cancelInvitation(inv.id)}>
            取消邀请
          </button>
        </div>
      ))}
    </div>
  );
}

接受或拒绝邀请

被邀请的用户可以查看和管理收到的所有邀请。

查看邀请

  1. 登录后,如果有待处理邀请,会在成员设置页面看到"我的邀请"按钮
  2. 点击按钮查看所有邀请
  3. 可以选择接受或拒绝
'use client';

import { authClient } from '~/lib/auth/client';
import { useEffect, useState } from 'react';

export function UserInvitations() {
  const [invitations, setInvitations] = useState<any[]>([]);

  useEffect(() => {
    loadUserInvitations();
  }, []);

  async function loadUserInvitations() {
    const result = await authClient.organization.listUserInvitations();
    if (result.data) {
      setInvitations(result.data);
    }
  }

  async function acceptInvitation(invitationId: string) {
    const result = await authClient.organization.acceptInvitation({
      invitationId,
    });

    if (!result.error) {
      alert('已加入工作区!');
      loadUserInvitations();
    }
  }

  async function rejectInvitation(invitationId: string) {
    const result = await authClient.organization.rejectInvitation({
      invitationId,
    });

    if (!result.error) {
      loadUserInvitations();
    }
  }

  return (
    <div>
      <h3>我的邀请</h3>
      {invitations.map((inv) => (
        <div key={inv.id}>
          <p>邀请加入: {inv.organizationName}</p>
          <p>角色: {inv.role}</p>
          <button onClick={() => acceptInvitation(inv.id)}>接受</button>
          <button onClick={() => rejectInvitation(inv.id)}>拒绝</button>
        </div>
      ))}
    </div>
  );
}

更改成员角色

管理员和所有者可以更改成员的角色。

更改步骤

  1. 打开工作区设置 → "成员" 标签页
  2. 找到要修改的成员
  3. 点击角色徽章或操作按钮
  4. 选择新角色
  5. 确认更改
'use client';

import { authClient } from '~/lib/auth/client';

export function UpdateMemberRole({
  memberId,
  newRole,
}: {
  memberId: string;
  newRole: 'member' | 'admin';
}) {
  async function handleUpdate() {
    const result = await authClient.organization.updateMemberRole({
      memberIdOrEmail: memberId,
      role: newRole,
    });

    if (result.error) {
      console.error('更新失败:', result.error);
      return;
    }

    alert('角色已更新!');
  }

  return <button onClick={handleUpdate}>更改为 {newRole}</button>;
}

移除成员

管理员和所有者可以将成员从工作区移除。

'use client';

import { authClient } from '~/lib/auth/client';

export function RemoveMember({ memberId }: { memberId: string }) {
  async function handleRemove() {
    const confirmed = window.confirm('确定要移除此成员吗?');
    if (!confirmed) return;

    const result = await authClient.organization.removeMember({
      memberIdOrEmail: memberId,
    });

    if (result.error) {
      console.error('移除失败:', result.error);
      return;
    }

    alert('成员已移除');
  }

  return <button onClick={handleRemove}>移除成员</button>;
}

离开工作区

非所有者的成员可以主动离开工作区。

离开步骤

  1. 打开工作区设置 → "成员" 标签页
  2. 在成员列表中找到自己
  3. 点击 "离开" 按钮
  4. 确认操作

注意: 所有者不能离开自己的工作区,必须先转移所有权或删除工作区。

'use client';

import { authClient } from '~/lib/auth/client';
import { useRouter } from 'next/navigation';

export function LeaveOrganization({ organizationId }: { organizationId: string }) {
  const router = useRouter();

  async function handleLeave() {
    const confirmed = window.confirm('确定要离开此工作区吗?离开后你将无法访问该工作区的任何内容。');
    if (!confirmed) return;

    const result = await authClient.organization.leave({
      organizationId,
    });

    if (result.error) {
      console.error('离开失败:', result.error);
      return;
    }

    // 跳转到工作区列表
    router.push('/spaces');
  }

  return <button onClick={handleLeave}>离开工作区</button>;
}

团队/群组管理

团队(Team/Group)是工作区内的子单位,用于组织成员和分配权限。

创建团队

管理员和所有者可以创建团队。

创建步骤

  1. 打开工作区设置 → "成员" 标签页 → "群组" 子标签
  2. 点击 "创建团队" 按钮
  3. 输入团队名称
  4. 点击 "创建"
'use client';

import { authClient } from '~/lib/auth/client';
import { useState } from 'react';

export function CreateTeamForm() {
  const [name, setName] = useState('');

  async function handleCreate() {
    const result = await authClient.organization.createTeam({
      name,
    });

    if (result.error) {
      console.error('创建失败:', result.error);
      return;
    }

    alert('团队已创建!');
    setName('');
  }

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleCreate(); }}>
      <input
        type="text"
        placeholder="团队名称"
        value={name}
        onChange={(e) => setName(e.target.value)}
        required
      />
      <button type="submit">创建团队</button>
    </form>
  );
}

添加成员到团队

管理员可以将工作区成员添加到特定团队。

'use client';

import { authClient } from '~/lib/auth/client';

export function AddMemberToTeam({
  teamId,
  memberIdOrEmail,
}: {
  teamId: string;
  memberIdOrEmail: string;
}) {
  async function handleAdd() {
    const result = await authClient.organization.addTeamMember({
      teamId,
      memberIdOrEmail,
    });

    if (result.error) {
      console.error('添加失败:', result.error);
      return;
    }

    alert('成员已添加到团队!');
  }

  return <button onClick={handleAdd}>添加到团队</button>;
}

从团队移除成员

'use client';

import { authClient } from '~/lib/auth/client';

export function RemoveMemberFromTeam({
  teamId,
  memberIdOrEmail,
}: {
  teamId: string;
  memberIdOrEmail: string;
}) {
  async function handleRemove() {
    const result = await authClient.organization.removeTeamMember({
      teamId,
      memberIdOrEmail,
    });

    if (result.error) {
      console.error('移除失败:', result.error);
      return;
    }

    alert('成员已从团队移除');
  }

  return <button onClick={handleRemove}>从团队移除</button>;
}

查看团队成员

'use client';

import { authClient } from '~/lib/auth/client';
import { useEffect, useState } from 'react';

export function TeamMembers({ teamId }: { teamId: string }) {
  const [members, setMembers] = useState<any[]>([]);

  useEffect(() => {
    loadMembers();
  }, []);

  async function loadMembers() {
    const result = await authClient.organization.listTeamMembers({
      teamId,
    });

    if (result.data) {
      setMembers(result.data);
    }
  }

  return (
    <div>
      <h3>团队成员</h3>
      <ul>
        {members.map((member) => (
          <li key={member.id}>
            {member.user.name} - {member.role}
          </li>
        ))}
      </ul>
    </div>
  );
}

删除团队

'use client';

import { authClient } from '~/lib/auth/client';

export function DeleteTeam({ teamId }: { teamId: string }) {
  async function handleDelete() {
    const confirmed = window.confirm('确定要删除此团队吗?');
    if (!confirmed) return;

    const result = await authClient.organization.deleteTeam({
      teamId,
    });

    if (result.error) {
      console.error('删除失败:', result.error);
      return;
    }

    alert('团队已删除');
  }

  return <button onClick={handleDelete}>删除团队</button>;
}

权限系统

ProductReady 实现了基于角色的访问控制(RBAC),支持细粒度的权限管理。

角色与权限对照表

资源OwnerAdminMemberViewer
工作区
查看设置
更新设置
删除工作区
成员管理
邀请成员(邮箱)
创建/撤销邀请链接
批准/拒绝加入请求
移除成员
更改角色
团队管理
创建团队
删除团队
管理团队成员
项目
创建项目
查看项目
编辑项目
删除项目
计费
查看计费
更新计费

在代码中检查权限

使用 usePermissions Hook 在 React 组件中检查权限:

'use client';

import { usePermissions } from '~/lib/auth/client';

export function ProjectActions() {
  const { can, canManage, role, isLoading } = usePermissions();

  if (isLoading) {
    return <div>加载中...</div>;
  }

  return (
    <div>
      <p>当前角色: {role}</p>
      
      {can('project', 'create') && (
        <button>创建项目</button>
      )}
      
      {can('issue', 'assign') && (
        <button>分配任务</button>
      )}
      
      {canManage && (
        <button>管理设置</button>
      )}
    </div>
  );
}

检查多个权限

// 检查是否满足任一权限
const hasAnyPermission = canAny([
  { resource: 'project', action: 'create' },
  { resource: 'project', action: 'update' },
]);

// 检查是否满足所有权限
const hasAllPermissions = canAll([
  { resource: 'member', action: 'create' },
  { resource: 'member', action: 'delete' },
]);

服务端权限检查

在 tRPC 路由或 API 路由中检查权限:

import { protectedProcedure } from '~/lib/trpc';
import { hasPermission } from '~/lib/auth/permissions';
import { TRPCError } from '@trpc/server';

export const projectsRouter = createTRPCRouter({
  create: protectedProcedure
    .input(z.object({ name: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const member = await ctx.auth.organization.getActiveMember();
      const role = member?.role;

      // 检查权限
      if (!hasPermission(role as Role, 'project', 'create')) {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: '没有权限创建项目',
        });
      }

      // 创建项目
      const project = await ctx.db.project.create({
        data: {
          name: input.name,
          organizationId: member.organizationId,
        },
      });

      return project;
    }),
});

工作区设置

打开工作区设置

方式一:点击侧边栏底部的"设置"按钮

方式二:在代码中触发设置模态框

'use client';

import { SettingsModal } from '~/components/dashboard/settings-modal';
import { useState } from 'react';

export function OpenSpaceSettings() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        工作区设置
      </button>
      <SettingsModal
        open={isOpen}
        onOpenChange={setIsOpen}
        defaultSection="space"
        defaultTab="general"
      />
    </>
  );
}

常规设置

在"常规"标签页中,可以:

  • 查看和编辑工作区名称
  • 查看和编辑工作区 Slug(用于 URL)
  • 查看创建时间和成员数量
  • 删除工作区(仅所有者可见)

计费设置

在"计费"标签页中,可以:

  • 查看当前套餐和用量
  • 升级或降级套餐
  • 管理支付方式
  • 查看计费历史

权限说明: 只有工作区所有者可以修改计费信息,管理员只能查看。


最佳实践

✅ 应该做的

  1. 明确角色分配

    • 只给少数人 Owner 角色(通常 1-2 人)
    • 给需要管理权限的人 Admin 角色
    • 大部分成员使用 Member 角色
    • 外部协作者使用 Viewer 角色
  2. 使用团队组织成员

    • 按部门或项目创建团队
    • 使用团队进行权限分组
    • 便于批量管理成员
  3. 定期审查成员

    • 定期检查成员列表
    • 移除不活跃的成员
    • 更新成员角色以匹配实际职责
  4. 安全的离职处理

    • 员工离职时立即移除其访问权限
    • 检查其创建的邀请并取消
    • 必要时更改 API 密钥

❌ 不应该做的

  1. 不要滥用 Owner 角色

    • Owner 拥有删除工作区的权限
    • 应该只分配给真正需要的人
  2. 不要忽视待处理邀请

    • 定期清理过期或无效的邀请
    • 避免安全风险
  3. 不要在没有备份的情况下删除工作区

    • 删除操作不可逆
    • 确保重要数据已备份

常见问题

如何转移工作区所有权?

目前需要通过以下步骤手动转移:

  1. 将新所有者设置为 Admin
  2. 原所有者与新所有者协商转移事宜
  3. 如需帮助,请联系技术支持

成员数量有限制吗?

取决于你的套餐:

  • 免费版: 最多 3 个成员
  • 专业版: 最多 10 个成员
  • 企业版: 无限制

如何批量邀请成员?

邮箱邀请仍然是一次一个,但邀请链接天然覆盖了批量邀请的场景——把一条链接发到群里、写进 onboarding 文档,任意数量的人都可以经这条链接加入,最多到你设置的使用次数上限为止。

离开工作区后还能重新加入吗?

可以,但需要重新收到邀请。离开不会删除你之前的贡献记录。


故障排除

无法邀请成员

可能原因

  1. 你没有邀请权限(需要 Admin 或 Owner)
  2. 邮箱格式不正确
  3. 已达到套餐成员上限
  4. 该用户已经是成员

解决方法

  • 检查你的角色权限
  • 验证邮箱格式
  • 升级套餐或移除不活跃成员

成员列表不显示

可能原因

  1. 网络问题
  2. 权限不足
  3. 数据加载错误

解决方法

  • 刷新页面
  • 检查控制台错误
  • 联系技术支持

邀请链接提示"链接已失效"

可能原因

  1. 管理员已撤销该链接
  2. 链接已过有效期
  3. 链接的使用次数已达上限

解决方法

  • 请工作区管理员生成一条新链接(成员标签页 → 邀请成员 → 邀请链接)

无法删除工作区

可能原因

  1. 你不是 Owner
  2. 输入的工作区名称不匹配

解决方法

  • 确认你是 Owner 角色
  • 准确输入工作区名称(区分大小写)

下一步

📚 更多资源

On this page