feat: workspace CRUD at platform scope + drop audit_logs

新增系统管理模块 /api/v1/platform/*:
- workspace 实体 CRUD(创建/列表/详情/更新/软删除)
- workspace 成员 CRUD(添加/列表/更新/移除)
- SystemAdminContext 依赖,仅 platform_role_id 指向 admin 角色的用户可访问
- /api/v1/auth/me 与 /auth/login 增 is_system_admin 派生字段
- 不变量:每个 workspace 至少保留一个 admin;系统管理员无法自我移除成员
- 软删除 workspace 级联软删除其成员

清理 audit_logs(无运行时写入,纯死特性):
- baseline 移除 audit_logs 建表与三索引(20 → 19 tables)
- 删除 AuditLogs 模型定义与 __init__.py 导出
- 清理 migrate_system_json / migrate_legacy_workspaces 中的 audit 写入与回填代码

API.md 增 §七系统管理,§七/§八/§九 顺延为 §八/§九/§十,附录 A/B 同步更新。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tao.chen
2026-08-04 17:20:42 +08:00
co-authored by Claude Fable 5
parent c263ae6a5f
commit 45f0ff534f
9 changed files with 806 additions and 243 deletions
@@ -6,7 +6,6 @@ import asyncio
import hashlib
import json
import os
from collections import defaultdict
from pathlib import Path
from typing import Any
from urllib.parse import quote
@@ -17,7 +16,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from common.db import create_database_engine, create_session_factory
from common.db.models import (
AuditLogs,
Roles,
Users,
WorkspaceMembers,
@@ -95,7 +93,6 @@ def new_stats() -> dict[str, int]:
"workspaces_updated": 0,
"workspace_members_inserted": 0,
"workspace_members_updated": 0,
"audit_logs_workspace_backfilled": 0,
}
@@ -266,44 +263,6 @@ async def migrate_members(
stats["workspace_members_updated"] += 1
async def backfill_audit_workspaces(
session: AsyncSession,
source: list[LegacyWorkspace],
workspace_ids: dict[str, str],
users: dict[str, Users],
stats: dict[str, int],
) -> None:
workspace_codes_by_username: dict[str, set[str]] = defaultdict(set)
for workspace in source:
for username in workspace.userIds:
workspace_codes_by_username[username].add(workspace.id)
workspace_by_user_id = {
users[username].user_id: workspace_ids[next(iter(codes))]
for username, codes in workspace_codes_by_username.items()
if len(codes) == 1
}
audit_logs = (
await session.scalars(
select(AuditLogs).where(AuditLogs.workspace_id.is_(None))
)
).all()
for audit_log in audit_logs:
if (
not isinstance(audit_log.detail_json, dict)
or audit_log.detail_json.get("migration_source")
!= "platform_data/system.json"
):
continue
workspace_id = workspace_by_user_id.get(
audit_log.actor_user_id
)
if workspace_id is not None:
audit_log.workspace_id = workspace_id
stats["audit_logs_workspace_backfilled"] += 1
async def run_migration(
database_url: str,
source: list[LegacyWorkspace],
@@ -339,13 +298,6 @@ async def run_migration(
users,
stats,
)
await backfill_audit_workspaces(
session,
source,
workspace_ids,
users,
stats,
)
if apply_changes:
await session.commit()