组织与团队管理
工作区(Space)管理、成员邀请、角色权限、团队协作完整指南
组织与团队管理
ProductReady 使用 Better Auth 组织插件实现了完整的多租户协作功能。本指南涵盖工作区管理、成员邀请、角色权限、团队协作等核心功能。
术语说明: 在 ProductReady 中,"工作区"(Space)和"组织"(Organization)是同一概念的不同表述。代码中使用 Organization,用户界面中使用 Space。
快速概览
核心功能:
- ✅ 创建和管理多个工作区
- ✅ 邀请成员加入工作区(邮箱邀请 或 可分享邀请链接)
- ✅ 链接加入可选管理员审批
- ✅ 基于角色的访问控制(RBAC)
- ✅ 团队/群组管理
- ✅ 工作区设置和计费管理
- ✅ 离开或删除工作区
角色体系:
- Owner(所有者): 拥有全部权限,包括删除工作区
- Admin(管理员): 几乎全部权限,不能删除工作区或修改计费
- Member(成员): 基本工作权限,可以创建和编辑内容
- Viewer(查看者): 只读权限,可以查看但不能修改
工作区(Space)管理
创建工作区
用户登录后可以创建多个工作区,用于不同的项目或团队。
创建工作区的方式:
- 点击左侧边栏顶部的工作区选择器
- 点击 "+ 新建工作区" 按钮
- 输入工作区名称和描述
- 点击 "创建"
'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>
);
}切换工作区
用户可以在多个工作区之间快速切换。
切换方式:
- 点击左侧边栏顶部的工作区选择器
- 从列表中选择目标工作区
- 页面自动切换到新工作区的上下文
'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>
);
}删除工作区
只有工作区所有者才能删除工作区。删除是不可逆的操作。
删除步骤:
- 打开工作区设置(点击侧边栏底部的"设置"按钮)
- 切换到"常规"标签页
- 滚动到"危险区域"
- 点击 "删除工作区" 按钮
- 输入工作区名称确认
- 点击 "确认删除"
警告: 删除工作区会永久删除所有相关数据,包括成员、团队、项目、文档等。此操作不可撤销!
'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>
);
}成员管理
邀请成员
管理员和所有者可以通过邮箱邀请新成员加入工作区。
邀请步骤:
- 打开工作区设置 → "成员" 标签页
- 点击 "邀请成员" 按钮
- 输入成员的邮箱地址
- 选择角色(Member、Admin、Viewer)
- 点击 "发送邀请"
被邀请者会收到邀请邮件,点击链接即可接受邀请。
'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 等主流协作工具的标配做法。
管理员和所有者可以生成一条可分享的邀请链接,不需要提前知道对方邮箱即可让其直接加入工作区。
创建步骤:
- 打开工作区设置 → "成员" 标签页
- 点击 "邀请成员" → "邀请链接" 标签
- 选择角色(Admin、Member 或 Viewer,不能选 Owner)、有效期(7 天 / 30 天 / 永不过期),可选设置使用次数上限
- 可选开启 "需要审批",开启后对方点击链接加入会进入待审批队列,而不是立即成为成员
- 点击 "创建邀请链接" 并复制链接分享出去
已有链接会排在最前面(复制是最常用的操作);创建新链接收进 "+ 新建链接" 按钮后面。链接可随时撤销——撤销后任何人打开都会看到"链接已失效"。
'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] 页面上一次明确的点击——未登录的访问者会先跳去注册,注册完自动跳回来继续加入流程。座位配额的判断和邮箱邀请一致:如果套餐的成员上限已满,加入者会看到升级提示,而不是被静默加入。
管理加入请求
当某条邀请链接开启了 "需要审批",点击加入不会立即成为成员,而是生成一条待审批的加入请求。管理员在同一个 "成员" 标签页里处理。
审批步骤:
- 打开工作区设置 → "成员" 标签页
- 待处理的请求会显示在成员列表上方的 "加入请求" 区块,带数量徽章
- 点击 "批准" 将其加入为成员(同样会检查座位配额),或点击 "拒绝" 驳回
无论批准还是拒绝,申请人都会收到一条站内通知。
查看待处理邀请
管理员可以查看和管理所有待处理的邀请。
查看步骤:
- 打开工作区设置 → "成员" 标签页
- 在成员列表下方可以看到"待处理邀请"部分
- 可以取消任何待处理的邀请
'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>
);
}接受或拒绝邀请
被邀请的用户可以查看和管理收到的所有邀请。
查看邀请:
- 登录后,如果有待处理邀请,会在成员设置页面看到"我的邀请"按钮
- 点击按钮查看所有邀请
- 可以选择接受或拒绝
'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>
);
}更改成员角色
管理员和所有者可以更改成员的角色。
更改步骤:
- 打开工作区设置 → "成员" 标签页
- 找到要修改的成员
- 点击角色徽章或操作按钮
- 选择新角色
- 确认更改
'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>;
}离开工作区
非所有者的成员可以主动离开工作区。
离开步骤:
- 打开工作区设置 → "成员" 标签页
- 在成员列表中找到自己
- 点击 "离开" 按钮
- 确认操作
注意: 所有者不能离开自己的工作区,必须先转移所有权或删除工作区。
'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)是工作区内的子单位,用于组织成员和分配权限。
创建团队
管理员和所有者可以创建团队。
创建步骤:
- 打开工作区设置 → "成员" 标签页 → "群组" 子标签
- 点击 "创建团队" 按钮
- 输入团队名称
- 点击 "创建"
'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),支持细粒度的权限管理。
角色与权限对照表
| 资源 | Owner | Admin | Member | Viewer |
|---|---|---|---|---|
| 工作区 | ||||
| 查看设置 | ✅ | ✅ | ✅ | ✅ |
| 更新设置 | ✅ | ✅ | ❌ | ❌ |
| 删除工作区 | ✅ | ❌ | ❌ | ❌ |
| 成员管理 | ||||
| 邀请成员(邮箱) | ✅ | ✅ | ❌ | ❌ |
| 创建/撤销邀请链接 | ✅ | ✅ | ❌ | ❌ |
| 批准/拒绝加入请求 | ✅ | ✅ | ❌ | ❌ |
| 移除成员 | ✅ | ✅ | ❌ | ❌ |
| 更改角色 | ✅ | ✅ | ❌ | ❌ |
| 团队管理 | ||||
| 创建团队 | ✅ | ✅ | ❌ | ❌ |
| 删除团队 | ✅ | ✅ | ❌ | ❌ |
| 管理团队成员 | ✅ | ✅ | ❌ | ❌ |
| 项目 | ||||
| 创建项目 | ✅ | ✅ | ❌ | ❌ |
| 查看项目 | ✅ | ✅ | ✅ | ✅ |
| 编辑项目 | ✅ | ✅ | ✅ | ❌ |
| 删除项目 | ✅ | ✅ | ❌ | ❌ |
| 计费 | ||||
| 查看计费 | ✅ | ✅ | ❌ | ❌ |
| 更新计费 | ✅ | ❌ | ❌ | ❌ |
在代码中检查权限
使用 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)
- 查看创建时间和成员数量
- 删除工作区(仅所有者可见)
计费设置
在"计费"标签页中,可以:
- 查看当前套餐和用量
- 升级或降级套餐
- 管理支付方式
- 查看计费历史
权限说明: 只有工作区所有者可以修改计费信息,管理员只能查看。
最佳实践
✅ 应该做的
-
明确角色分配
- 只给少数人 Owner 角色(通常 1-2 人)
- 给需要管理权限的人 Admin 角色
- 大部分成员使用 Member 角色
- 外部协作者使用 Viewer 角色
-
使用团队组织成员
- 按部门或项目创建团队
- 使用团队进行权限分组
- 便于批量管理成员
-
定期审查成员
- 定期检查成员列表
- 移除不活跃的成员
- 更新成员角色以匹配实际职责
-
安全的离职处理
- 员工离职时立即移除其访问权限
- 检查其创建的邀请并取消
- 必要时更改 API 密钥
❌ 不应该做的
-
不要滥用 Owner 角色
- Owner 拥有删除工作区的权限
- 应该只分配给真正需要的人
-
不要忽视待处理邀请
- 定期清理过期或无效的邀请
- 避免安全风险
-
不要在没有备份的情况下删除工作区
- 删除操作不可逆
- 确保重要数据已备份
常见问题
如何转移工作区所有权?
目前需要通过以下步骤手动转移:
- 将新所有者设置为 Admin
- 原所有者与新所有者协商转移事宜
- 如需帮助,请联系技术支持
成员数量有限制吗?
取决于你的套餐:
- 免费版: 最多 3 个成员
- 专业版: 最多 10 个成员
- 企业版: 无限制
如何批量邀请成员?
邮箱邀请仍然是一次一个,但邀请链接天然覆盖了批量邀请的场景——把一条链接发到群里、写进 onboarding 文档,任意数量的人都可以经这条链接加入,最多到你设置的使用次数上限为止。
离开工作区后还能重新加入吗?
可以,但需要重新收到邀请。离开不会删除你之前的贡献记录。
故障排除
无法邀请成员
可能原因:
- 你没有邀请权限(需要 Admin 或 Owner)
- 邮箱格式不正确
- 已达到套餐成员上限
- 该用户已经是成员
解决方法:
- 检查你的角色权限
- 验证邮箱格式
- 升级套餐或移除不活跃成员
成员列表不显示
可能原因:
- 网络问题
- 权限不足
- 数据加载错误
解决方法:
- 刷新页面
- 检查控制台错误
- 联系技术支持
邀请链接提示"链接已失效"
可能原因:
- 管理员已撤销该链接
- 链接已过有效期
- 链接的使用次数已达上限
解决方法:
- 请工作区管理员生成一条新链接(成员标签页 → 邀请成员 → 邀请链接)
无法删除工作区
可能原因:
- 你不是 Owner
- 输入的工作区名称不匹配
解决方法:
- 确认你是 Owner 角色
- 准确输入工作区名称(区分大小写)
下一步
📚 更多资源: