feat(roundtable): 业务链条配置增加管理员权限,管理员可配所有链条

- 后端 store/路由:scope=all(仅管理员)列全量;get/update/delete 加 is_admin 旁路,管理员可操作任意 owner 链条且不转移归属;非管理员请求 all 退化为 mine
- 前端列表页/会商弹窗:管理员多「全部链条」筛选 tab,canManage=isAdmin||isMine 放开发布/设置/删除
- 编辑器:管理员可见发布按钮;按 chainId 找不到本地列表时直接 getChain 拉取(修复管理员从「全部链条」进编辑器空白),并修掉自身无链条时加载卡死的边界

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-21 17:17:19 +08:00
1 parent 48744441ba
commit f13a1bb7fe
6 files changed
+113 -45

No files matched your search

@@ -70,36 +70,41 @@ class RoundtableChainRepository:
}
async def list_chains(
self, user_id: str, *, scope: str = "mine", limit: int = 200
self, user_id: str, *, scope: str = "mine", limit: int = 200, is_admin: bool = False
) -> list[dict[str, Any]]:
"""列出链条。
``scope``:
- ``"mine"``(默认):仅当前用户自己的链条;
- ``"public"``:所有**已发布**(``is_public=True``)链条,含本人发布的。
- ``"public"``:所有**已发布**(``is_public=True``)链条,含本人发布的;
- ``"all"``:所有账号的全部链条(**仅管理员**,由路由层校验后传 ``is_admin=True``;
非管理员误传则按 ``"mine"`` 退化,绝不越权)。
其余值按 ``"mine"`` 处理。``is_mine`` 标记每条是否本人所有,供前端判断可否编辑。
"""
if scope == "public":
cond = RoundtableChainRow.is_public.is_(True)
elif scope == "all" and is_admin:
cond = None
else:
cond = RoundtableChainRow.user_id == user_id
stmt = (
select(RoundtableChainRow)
.where(cond)
.order_by(RoundtableChainRow.updated_at.desc())
.limit(limit)
)
stmt = select(RoundtableChainRow)
if cond is not None:
stmt = stmt.where(cond)
stmt = stmt.order_by(RoundtableChainRow.updated_at.desc()).limit(limit)
async with self._sf() as session:
result = await session.execute(stmt)
return [self._to_dict(row, user_id) for row in result.scalars()]
async def get_chain(self, chain_id: str, user_id: str) -> dict[str, Any] | None:
"""取一条链条:owner 可见,**已发布的公共链条任何人可见**(供选入研讨)。"""
async def get_chain(
self, chain_id: str, user_id: str, *, is_admin: bool = False
) -> dict[str, Any] | None:
"""取一条链条:owner 可见,**已发布的公共链条任何人可见**(供选入研讨);
**管理员可见任意链条**(``is_admin=True``,供后台管理配置)。"""
async with self._sf() as session:
row = await session.get(RoundtableChainRow, chain_id)
if row is None:
return None
if row.user_id != user_id and not bool(getattr(row, "is_public", False)):
if not is_admin and row.user_id != user_id and not bool(getattr(row, "is_public", False)):
return None
return self._to_dict(row, user_id)
@@ -140,10 +145,12 @@ class RoundtableChainRepository:
seat_mode=_UNSET,
gather_first=_UNSET,
is_public=_UNSET,
is_admin: bool = False,
) -> dict[str, Any] | None:
async with self._sf() as session:
row = await session.get(RoundtableChainRow, chain_id)
if row is None or row.user_id != user_id:
# 管理员可改任意链条;否则仅 owner 本人。不变更 user_id(不转移归属)。
if row is None or (not is_admin and row.user_id != user_id):
return None
if title is not _UNSET and title is not None:
row.title = str(title).strip()
@@ -168,10 +175,11 @@ class RoundtableChainRepository:
await session.refresh(row)
return self._to_dict(row, user_id)
async def delete_chain(self, chain_id: str, user_id: str) -> bool:
async def delete_chain(self, chain_id: str, user_id: str, *, is_admin: bool = False) -> bool:
async with self._sf() as session:
row = await session.get(RoundtableChainRow, chain_id)
if row is None or row.user_id != user_id:
# 管理员可删任意链条;否则仅 owner 本人。
if row is None or (not is_admin and row.user_id != user_id):
return False
await session.delete(row)
await session.commit()