Develop #16

Merged
tao.chen merged 273 commits from develop into main 2026-08-21 10:42:09 +08:00
2 changed files with 74 additions and 3 deletions
Showing only changes of commit d813508461 - Show all commits
+23 -1
View File
@@ -500,6 +500,7 @@ Base 前缀 `/api/v1/admin`。
| 方法 | 路径 | 说明 |
|---|---|---|
| `GET` | `/api/v1/platform/employees` | 列全平台未软删员工;包含停用、锁定及无平台角色用户 |
| `POST` | `/api/v1/platform/employees` | 创建平台员工账号(返回 201);不自动加入任何 workspace |
| `GET` | `/api/v1/platform/workspaces` | 列 workspace(`active`/`archived`);已软删的过滤掉 |
| `POST` | `/api/v1/platform/workspaces` | 创建 workspace(返回 201);创建者自动成为 admin 成员 |
| `GET` | `/api/v1/platform/workspaces/{workspace_id}` | 单个 workspace(含已 disabled 的,用于恢复) |
@@ -596,7 +597,8 @@ Base 前缀 `/api/v1/admin`。
| `role_code` | string | 是 | `admin` \| `developer` | **不可填 `system_admin`**(那是用户级身份,不是 workspace 角色) |
- 服务端默认 `member_status='active'`。
- 用户不存在 → 404;用户已是该 workspace 成员 → 409。
- 用户不存在或已软删除 → 404;用户状态不是 `active` → 409;用户已是该 workspace 成员 → 409。
- 新员工必须先通过 `POST /api/v1/platform/employees` 或现有 workspace 员工创建接口建立用户账号。
### 7.6 `PATCH /api/v1/platform/workspaces/{workspace_id}/members/{user_id}`
@@ -647,6 +649,26 @@ Base 前缀 `/api/v1/admin`。
}
```
### 7.9 `POST /api/v1/platform/employees`
只创建平台员工账号,不创建任何 `workspace_members` 记录。调用者必须是系统管理员;已认证但不是系统管理员时返回 `403`。
- **请求体字段**:
| 字段 | 类型 | 必填 | 限制 | 说明 |
|---|---|---|---|---|
| `username` | string | 是 | 2~64 字符 | 全平台唯一登录名 |
| `display_name` | string | 是 | 1~100 字符 | 显示名称 |
| `email` | string | 否 | ≤255 字符 | 邮箱,全平台唯一 |
| `password` | string | 是 | 8~72 字符 | 登录密码 |
- 新用户状态固定为 `active`,且不分配平台角色。
- 密码使用 bcrypt 哈希保存;响应不包含 `password` 或 `password_hash`。
- 用户名或邮箱重复 → 409。
- 创建成功后,可调用 `POST /api/v1/platform/workspaces/{workspace_id}/members` 将用户加入指定 workspace。
- **响应 201**:字段与 §7.8 的员工元素一致,其中 `role_code`、`role_name` 均为 `null`,`meta` 为空对象。
---
## 八、Jupyter 路由
+51 -2
View File
@@ -28,6 +28,7 @@ Workspace membership CRUD::
Platform employee roster::
GET /employees — list all non-deleted users
POST /employees — create a user without workspace membership
Invariants
----------
@@ -50,10 +51,11 @@ from typing import Any, Literal
from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import func, select, update
from sqlalchemy import func, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from backend.dependencies import current_user, database_session
from common.auth.passwords import hash_password
from common.db.models import Roles, Users, WorkspaceMembers, Workspaces
from common.ids import new_ulid
@@ -111,6 +113,15 @@ class MemberUpdate(BaseModel):
member_status: Literal["active", "disabled", "locked"] | None = None
class PlatformEmployeeCreate(BaseModel):
model_config = ConfigDict(extra="forbid")
username: str = Field(min_length=2, max_length=64)
display_name: str = Field(min_length=1, max_length=100)
email: str | None = Field(default=None, max_length=255)
password: str = Field(min_length=8, max_length=72)
# ---------------------------------------------------------------------------
# System-admin context dependency
# ---------------------------------------------------------------------------
@@ -302,6 +313,39 @@ async def list_platform_employees(
)
@router.post("/employees", status_code=status.HTTP_201_CREATED)
async def create_platform_employee(
payload: PlatformEmployeeCreate,
context: SystemAdminContext = Depends(system_admin_context),
session: AsyncSession = Depends(database_session),
) -> dict[str, Any]:
"""Create a platform user without assigning workspace membership."""
username = payload.username.strip()
display_name = payload.display_name.strip()
duplicate_conditions = [Users.username == username]
if payload.email:
duplicate_conditions.append(Users.email == payload.email.strip())
duplicate = await session.scalar(
select(Users.user_id).where(or_(*duplicate_conditions))
)
if duplicate is not None:
raise HTTPException(status.HTTP_409_CONFLICT, "用户名或邮箱已存在")
user = Users(
user_id=new_ulid(),
username=username,
display_name=display_name,
email=payload.email.strip() if payload.email else None,
password_hash=hash_password(payload.password),
status="active",
platform_role_id=None,
)
session.add(user)
await session.flush()
await session.refresh(user)
return _envelope(context.request_id, platform_employee_payload(user, None))
# ---------------------------------------------------------------------------
# Workspace CRUD
# ---------------------------------------------------------------------------
@@ -502,8 +546,13 @@ async def add_member(
"""Add a user to a workspace. The new row starts with member_status='active'."""
await _load_workspace(session, workspace_id)
user = await session.get(Users, payload.user_id)
if user is None:
if user is None or user.is_deleted != 0:
raise HTTPException(status.HTTP_404_NOT_FOUND, "用户不存在")
if user.status != "active":
raise HTTPException(
status.HTTP_409_CONFLICT,
f"用户状态为 {user.status},无法加入 workspace",
)
role = await _load_role_by_code(session, payload.role_code)
duplicate = await session.scalar(
select(WorkspaceMembers.user_id).where(