LuoChinWen 2 месяцев назад
Родитель
Сommit
c9f743e882

+ 0 - 169
ERROR_MESSAGE_STYLE_FIX.md

@@ -1,169 +0,0 @@
-# 错误消息样式优化文档
-
-## 概述
-
-将项目中所有红底红字(或红底白字)的错误消息样式统一优化为白底红字,提高可读性和视觉舒适度。
-
-## 问题
-
-之前的错误消息使用红色背景 + 白色/红色文字,存在以下问题:
-1. **对比度过高**:红底白字过于刺眼
-2. **可读性差**:红底红字几乎无法阅读
-3. **视觉疲劳**:长时间查看容易造成眼睛疲劳
-4. **不够现代**:现代设计趋向于使用浅色背景 + 彩色边框
-
-## 解决方案
-
-### 新的错误消息样式
-- **背景**:白色(`--color-neutral-background`)
-- **标题/图标**:红色(`--color-negative-content`)
-- **消息文字**:深灰色(`--color-neutral-content`)
-- **边框**:红色(`--color-negative-border`)
-- **左侧粗边框**:深红色(`--color-negative-border-bold`,4px)
-
-### 样式代码模板
-
-```scss
-.errorMessage {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  padding: 12px 16px;
-  background: var(--color-neutral-background);
-  color: var(--color-negative-content);
-  border: 1px solid var(--color-negative-border);
-  border-left: 4px solid var(--color-negative-border-bold);
-  border-radius: 8px;
-  font-size: 14px;
-
-  svg {
-    flex-shrink: 0;
-    color: var(--color-negative-content);
-  }
-
-  span {
-    color: var(--color-neutral-content);
-  }
-}
-```
-
-## 修改的文件
-
-### 1. Toast 组件
-**文件**: `web/apps/lq_label/src/components/toast/toast.module.scss`
-
-**修改内容**:
-- 错误类型 Toast:白底红字
-- 成功类型 Toast:白底绿字
-- 警告类型 Toast:白底橙字
-- 信息类型 Toast:保持深色背景
-
-### 2. 任务列表视图
-**文件**: `web/apps/lq_label/src/views/task-list-view/task-list-view.module.scss`
-
-**修改内容**:
-- `.errorMessage` 样式:从红底白字改为白底红字
-- 添加左侧粗边框(4px)
-- 图标和文字颜色分离
-
-### 3. 项目列表视图
-**文件**: `web/apps/lq_label/src/views/project-list-view/project-list-view.module.scss`
-
-**修改内容**:
-- `.errorMessage` 样式:从红底白字改为白底红字
-- 添加左侧粗边框(4px)
-- 图标和文字颜色分离
-
-### 4. 项目标注视图
-**文件**: `web/apps/lq_label/src/views/project-annotation-view/project-annotation-view.module.scss`
-
-**修改内容**:
-- `.errorBanner` 样式:从红底白字改为白底红字
-- 添加左侧粗边框(4px)
-
-## 设计原则
-
-### 1. 视觉层次
-- **主要信息**(标题/图标):使用主题色(红色/绿色/橙色)
-- **次要信息**(消息内容):使用中性色(深灰色)
-- **背景**:使用浅色(白色)
-
-### 2. 状态区分
-- **左侧粗边框**:4px 宽,使用深色主题色
-- **整体边框**:1px 宽,使用主题色
-- **背景**:统一使用白色
-
-### 3. 可读性优化
-- **对比度**:确保文字与背景有足够对比度
-- **颜色搭配**:避免同色系背景和文字
-- **视觉舒适**:使用柔和的颜色组合
-
-## 颜色变量说明
-
-### 错误/负面状态
-- `--color-negative-content`: 红色文字(用于标题和图标)
-- `--color-negative-border`: 红色边框
-- `--color-negative-border-bold`: 深红色粗边框
-
-### 成功/正面状态
-- `--color-positive-content`: 绿色文字
-- `--color-positive-border`: 绿色边框
-- `--color-positive-border-bold`: 深绿色粗边框
-
-### 警告状态
-- `--color-warning-content`: 橙色文字
-- `--color-warning-border`: 橙色边框
-- `--color-warning-border-bold`: 深橙色粗边框
-
-### 中性状态
-- `--color-neutral-background`: 白色背景
-- `--color-neutral-content`: 深灰色文字
-
-## 优势
-
-### 1. 可读性提升
-✅ 白色背景 + 深色文字,对比度适中
-✅ 标题和内容颜色分离,层次清晰
-✅ 避免了红底红字的可读性问题
-
-### 2. 视觉舒适
-✅ 减少视觉疲劳
-✅ 颜色搭配更加和谐
-✅ 符合现代设计趋势
-
-### 3. 一致性
-✅ 所有错误消息样式统一
-✅ 使用项目设计令牌
-✅ 与 Toast 组件样式保持一致
-
-### 4. 可维护性
-✅ 使用 CSS 变量,易于主题切换
-✅ 样式代码清晰,易于理解
-✅ 遵循项目规范
-
-## 测试建议
-
-重启前端服务器后,测试以下场景:
-
-1. **任务列表**:触发加载错误,查看错误消息样式
-2. **项目列表**:触发加载错误,查看错误消息样式
-3. **项目标注**:触发标注错误,查看错误横幅样式
-4. **Toast 通知**:触发各种类型的 Toast(成功、错误、警告、信息)
-
-## 相关文件
-
-- `web/apps/lq_label/src/components/toast/toast.module.scss`
-- `web/apps/lq_label/src/views/task-list-view/task-list-view.module.scss`
-- `web/apps/lq_label/src/views/project-list-view/project-list-view.module.scss`
-- `web/apps/lq_label/src/views/project-annotation-view/project-annotation-view.module.scss`
-
-## 更新日期
-
-2025-01-23
-
-## 注意事项
-
-1. **不要修改按钮样式**:删除按钮等危险操作按钮仍然使用红色背景
-2. **不要修改表单错误**:表单字段的错误提示保持红色文字即可
-3. **使用设计令牌**:确保使用项目中实际存在的 CSS 变量
-4. **保持一致性**:新增错误消息时,使用相同的样式模板

+ 0 - 207
OAUTH_DUPLICATE_CALL_FIX.md

@@ -1,207 +0,0 @@
-# OAuth 回调重复调用问题修复
-
-## 问题描述
-
-在开发环境中,OAuth 回调端点 `/api/oauth/callback` 被调用了两次,导致第二次调用时出现错误:
-
-```json
-{
-  "detail": "OAuth 登录失败: 无效的令牌响应格式: {'error': 'invalid_grant', 'error_description': '授权码已被使用'}"
-}
-```
-
-## 根本原因
-
-### React StrictMode
-
-在开发环境中,React 18 的 StrictMode 会**故意**渲染组件两次,以帮助发现副作用问题。这导致 `useEffect` 被执行两次。
-
-```tsx
-// main.tsx
-<StrictMode>
-  <BrowserRouter>
-    <App />
-  </BrowserRouter>
-</StrictMode>
-```
-
-### 错误的修复尝试
-
-最初尝试使用 `useState` 来防止重复调用:
-
-```tsx
-const [isProcessing, setIsProcessing] = useState(false);
-
-useEffect(() => {
-  if (isProcessing) {
-    return; // 试图阻止重复调用
-  }
-  
-  setIsProcessing(true);
-  // ... 处理逻辑
-}, [searchParams, navigate, setAuth, isProcessing]); // ❌ 问题在这里!
-```
-
-**问题**:`isProcessing` 被添加到依赖数组中,当它从 `false` 变为 `true` 时,会触发 `useEffect` 重新运行,从而绕过了防重复调用的检查。
-
-## 正确的解决方案
-
-使用 `useRef` 而不是 `useState` 来跟踪处理状态:
-
-```tsx
-const isProcessingRef = useRef(false);
-
-useEffect(() => {
-  // 防止 React StrictMode 导致的重复调用
-  if (isProcessingRef.current) {
-    console.log('Already processing, skipping duplicate call');
-    return;
-  }
-
-  const processCallback = async () => {
-    isProcessingRef.current = true;
-    console.log('Starting OAuth callback processing...');
-    
-    try {
-      // ... OAuth 处理逻辑
-    } catch (err) {
-      // ... 错误处理
-    }
-  };
-
-  processCallback();
-}, [searchParams, navigate, setAuth]); // ✅ 不包含 isProcessingRef
-```
-
-### 为什么 useRef 有效?
-
-1. **不触发重渲染**:修改 `ref.current` 不会触发组件重渲染
-2. **不在依赖数组中**:`useRef` 返回的对象在组件生命周期内保持不变,不需要添加到依赖数组
-3. **跨渲染持久化**:即使组件被渲染两次,`ref` 的值也会保持
-
-## 执行流程
-
-### StrictMode 下的执行顺序
-
-```
-1. 第一次渲染
-   - isProcessingRef.current = false
-   - 检查通过,开始处理
-   - isProcessingRef.current = true
-   - 调用 /api/oauth/callback (第一次)
-
-2. StrictMode 触发第二次渲染
-   - isProcessingRef.current = true (保持不变)
-   - 检查失败,跳过处理
-   - 不调用 /api/oauth/callback
-
-结果:只调用一次 ✅
-```
-
-### 使用 useState 的错误流程
-
-```
-1. 第一次渲染
-   - isProcessing = false
-   - 检查通过,开始处理
-   - setIsProcessing(true)
-   - 调用 /api/oauth/callback (第一次)
-
-2. isProcessing 变化触发 useEffect 重新运行
-   - isProcessing = true
-   - 检查失败,跳过... 等等!
-   
-3. StrictMode 触发第二次渲染
-   - isProcessing = false (重置)
-   - 检查通过,开始处理
-   - 调用 /api/oauth/callback (第二次) ❌
-
-结果:调用两次 ❌
-```
-
-## 测试验证
-
-### 开发环境测试
-
-1. 启动前端:`cd web && yarn nx serve lq_label`
-2. 访问:`http://localhost:4200/login`
-3. 点击"使用 SSO 登录"
-4. 完成 OAuth 登录
-5. 查看浏览器控制台:
-
-```
-OAuth callback URL parameters: {code: "...", state: "..."}
-Starting OAuth callback processing...
-Code: NhbmBfgF4XMJcsAlykbs2GOAec_0ylqu9JcAKvCcffA
-State: O_Fo8TTBRAHdTxw4GTZD_ZmvAz0fD1KnQIo4TE337fc
-Exchanging code for tokens...
-Already processing, skipping duplicate call  ← 第二次调用被阻止
-Token exchange successful
-```
-
-6. 查看后端日志:
-
-```
-INFO:     127.0.0.1:xxxxx - "GET /api/oauth/callback?code=...&state=... HTTP/1.1" 200 OK
-```
-
-**只有一次请求!** ✅
-
-### 生产环境
-
-在生产环境中,StrictMode 会被禁用,所以不会有重复调用问题。但保留这个修复不会有任何负面影响。
-
-## 其他解决方案(不推荐)
-
-### 方案 1:禁用 StrictMode(不推荐)
-
-```tsx
-// main.tsx
-// ❌ 不推荐:失去 StrictMode 的开发时检查
-<BrowserRouter>
-  <App />
-</BrowserRouter>
-```
-
-**缺点**:失去 React 18 的开发时检查和警告。
-
-### 方案 2:使用 AbortController(过度设计)
-
-```tsx
-useEffect(() => {
-  const controller = new AbortController();
-  
-  const processCallback = async () => {
-    try {
-      const response = await fetch('/api/oauth/callback', {
-        signal: controller.signal
-      });
-      // ...
-    } catch (err) {
-      if (err.name === 'AbortError') return;
-      // ...
-    }
-  };
-  
-  processCallback();
-  
-  return () => controller.abort();
-}, [searchParams]);
-```
-
-**缺点**:更复杂,且第一次请求仍然会被发送(只是被取消)。
-
-## 总结
-
-- ✅ **使用 `useRef`** 跟踪处理状态
-- ✅ **不要将 ref 添加到依赖数组**
-- ✅ **保留 StrictMode** 以获得开发时检查
-- ✅ **添加日志** 以便调试
-
-这个修复简单、有效,且不影响生产环境的行为。
-
----
-
-**修复日期**: 2024-01-22  
-**文件**: `web/apps/lq_label/src/components/oauth-callback/oauth-callback.tsx`  
-**状态**: ✅ 已修复

+ 0 - 212
OAUTH_IMPROVEMENTS.md

@@ -1,212 +0,0 @@
-# OAuth 登录优化总结
-
-## 完成的优化
-
-### 1. 修复 React StrictMode 重复调用问题 ✅
-
-**问题**:
-- OAuth 回调组件在开发环境下被调用两次
-- 导致授权码被使用两次,第二次调用失败
-
-**解决方案**:
-- 使用 `useRef` 替代 `useState` 来跟踪处理状态
-- 移除 `isProcessing` 从 `useEffect` 依赖数组
-- 添加日志以便调试
-
-**修改文件**:
-- `web/apps/lq_label/src/components/oauth-callback/oauth-callback.tsx`
-
-**关键代码**:
-```typescript
-const isProcessingRef = useRef(false);
-
-useEffect(() => {
-  if (isProcessingRef.current) {
-    console.log('Already processing, skipping duplicate call');
-    return;
-  }
-  
-  const processCallback = async () => {
-    isProcessingRef.current = true;
-    // ... 处理逻辑
-  };
-  
-  processCallback();
-}, [searchParams, navigate, setAuth]); // 不包含 isProcessing
-```
-
-### 2. 优化 401 错误处理 ✅
-
-**改进**:
-- 所有 401 错误都会自动跳转到登录页面
-- 区分 token 过期和其他认证失败
-- 清除本地存储的认证数据
-- 显示友好的错误提示
-
-**修改文件**:
-- `web/apps/lq_label/src/services/api.ts`
-
-**处理逻辑**:
-1. **Token 过期**(`error_type === 'token_expired'`):
-   - 尝试使用 refresh token 刷新
-   - 刷新成功:重试原请求
-   - 刷新失败:清除数据,跳转登录页
-
-2. **其他 401 错误**(无效凭证等):
-   - 直接清除认证数据
-   - 显示"认证失败,请重新登录"
-   - 跳转到登录页面
-
-### 3. 优化登录界面设计 ✅
-
-**设计改进**:
-- ❌ 移除渐变色背景
-- ✅ 使用设计令牌(Design Tokens)
-- ✅ 登录表单放在右侧 1/3
-- ✅ 左侧 2/3 显示品牌信息和特性
-- ✅ 添加背景图案增强视觉效果
-- ✅ 添加功能特性卡片
-- ✅ 优化按钮交互效果
-- ✅ 响应式设计(移动端自适应)
-
-**修改文件**:
-- `web/apps/lq_label/src/components/login-form/login-form.tsx`
-- `web/apps/lq_label/src/components/login-form/login-form.module.scss`
-
-**新设计特点**:
-
-1. **左侧品牌区域**:
-   - 大号品牌 Logo(120x120px)
-   - 品牌名称和描述
-   - 背景几何图案(低透明度)
-   - 4 个功能特性卡片:
-     * 高效标注
-     * 团队协作
-     * 实时同步
-     * 安全可靠
-
-2. **右侧登录区域**:
-   - 清晰的标题"欢迎回来"
-   - SSO 登录按钮(悬停效果)
-   - 分隔线"或使用账号密码"
-   - 用户名和密码输入框
-   - 登录按钮(阴影和悬停效果)
-   - 注册链接
-
-3. **视觉细节**:
-   - Logo 带有光晕效果
-   - 按钮悬停时有平滑过渡
-   - 输入框聚焦时有边框高亮
-   - 卡片式布局,层次分明
-   - 使用阴影增强深度感
-
-**新布局**:
-```
-┌─────────────────────────────────────────────────────────────┐
-│                          │                                   │
-│    [背景图案]            │         欢迎回来                  │
-│                          │    登录您的账号以继续使用标注平台 │
-│      [标 Logo]           │                                   │
-│     标注平台             │    [使用 SSO 登录]                │
-│  高效、智能的数据标注... │                                   │
-│                          │    ─── 或使用账号密码 ───         │
-│  ┌────┬────┐             │                                   │
-│  │高效│团队│             │    用户名: [____________]         │
-│  │标注│协作│             │    密码:   [____________]         │
-│  ├────┼────┤             │                                   │
-│  │实时│安全│             │    [登录]                         │
-│  │同步│可靠│             │                                   │
-│  └────┴────┘             │    还没有账号?立即注册           │
-│                          │                                   │
-└─────────────────────────────────────────────────────────────┘
-      左侧 (2/3)                    右侧 (1/3)
-```
-
-## 测试建议
-
-### 1. 测试 OAuth 重复调用修复
-
-1. 打开浏览器开发者工具(Console 标签)
-2. 访问 `http://localhost:4200/login`
-3. 点击"使用 SSO 登录"
-4. 完成 OAuth 登录
-5. 检查控制台日志:
-   - 应该只看到一次 "Starting OAuth callback processing..."
-   - 如果看到 "Already processing, skipping duplicate call",说明修复生效
-
-### 2. 测试 401 错误处理
-
-**测试 Token 过期**:
-1. 登录系统
-2. 等待 15 分钟(access token 过期)
-3. 执行任何 API 操作
-4. 应该自动刷新 token 并继续操作
-
-**测试无效 Token**:
-1. 登录系统
-2. 手动修改 localStorage 中的 access_token
-3. 执行任何 API 操作
-4. 应该显示"认证失败,请重新登录"并跳转到登录页
-
-### 3. 测试登录界面
-
-**桌面端**:
-1. 访问 `http://localhost:4200/login`
-2. 检查布局:
-   - 左侧显示品牌信息(占 2/3)
-   - 右侧显示登录表单(占 1/3)
-   - 无渐变色背景
-   - 使用设计令牌颜色
-
-**移动端**:
-1. 调整浏览器窗口宽度 < 768px
-2. 检查布局:
-   - 品牌信息在上方
-   - 登录表单在下方
-   - 垂直排列
-
-## 配置文件
-
-### OAuth 配置(`backend/config.yaml`)
-
-```yaml
-oauth:
-  enabled: true
-  base_url: "http://192.168.92.61:8000"
-  client_id: "sRyfcQwNVoFimigzuuZxhqd36fPkVN5G"
-  client_secret: "96RuKb4obAn9bQ9i5NtINiKBMvF_9uuCR7eNzD9dWQMbOWZaV3P593-8yLOqzWRd"
-  redirect_uri: "http://localhost:4200/auth/callback"
-  scope: "profile email"
-  authorize_endpoint: "/oauth/login"
-  token_endpoint: "/oauth/token"
-  userinfo_endpoint: "/oauth/userinfo"
-```
-
-## 相关文档
-
-- [OAuth 登录测试指南](./OAUTH_LOGIN_TEST_GUIDE.md)
-- [OAuth 集成指南](./OAUTH_INTEGRATION_GUIDE.md)
-- [前端认证指南](./web/apps/lq_label/FRONTEND_AUTH_GUIDE.md)
-- [后端 JWT 认证指南](./backend/JWT_AUTHENTICATION_GUIDE.md)
-
-## 技术栈
-
-- **前端**:React 18, TypeScript, Jotai, React Router
-- **后端**:FastAPI, Python, SQLAlchemy
-- **认证**:JWT, OAuth 2.0
-- **样式**:SCSS Modules, Design Tokens
-
-## 下一步
-
-1. ✅ 修复 React StrictMode 重复调用
-2. ✅ 优化 401 错误处理
-3. ✅ 优化登录界面设计
-4. 🔄 测试完整的 OAuth 登录流程
-5. 🔄 验证用户同步到本地数据库
-6. 🔄 测试 token 刷新机制
-
----
-
-**更新日期**: 2024-01-22  
-**状态**: ✅ 完成  
-**版本**: 1.0

+ 0 - 0
OAUTH_INTEGRATION_COMPLETE.md


+ 0 - 250
OAUTH_INTEGRATION_GUIDE.md

@@ -1,250 +0,0 @@
-# OAuth 2.0 单点登录对接方案
-
-## 概述
-
-本文档详细说明如何将标注平台与 OAuth 2.0 认证中心集成,实现单点登录(SSO)功能。
-
-## OAuth 2.0 授权码模式流程
-
-```
-┌─────────┐                                           ┌──────────────┐
-│         │                                           │              │
-│  用户   │                                           │  标注平台    │
-│         │                                           │  (Client)    │
-└────┬────┘                                           └──────┬───────┘
-     │                                                       │
-     │  1. 访问标注平台                                      │
-     ├──────────────────────────────────────────────────────>│
-     │                                                       │
-     │  2. 重定向到 OAuth 登录页                             │
-     │<──────────────────────────────────────────────────────┤
-     │                                                       │
-     │                                                       │
-     │  ┌──────────────────────────────────────────┐        │
-     │  │  OAuth 认证中心                           │        │
-     │  │  (http://192.168.92.61:8000)             │        │
-     │  └──────────────────────────────────────────┘        │
-     │                                                       │
-     │  3. 用户登录并授权                                    │
-     ├──────────────────────────────────────────────────────>│
-     │                                                       │
-     │  4. 返回授权码 (code)                                 │
-     │<──────────────────────────────────────────────────────┤
-     │                                                       │
-     │  5. 携带授权码回调标注平台                            │
-     ├──────────────────────────────────────────────────────>│
-     │                                                       │
-     │                                                       │  6. 用授权码换取 token
-     │                                                       ├────────────────────>
-     │                                                       │                     OAuth
-     │                                                       │  7. 返回 access_token
-     │                                                       │<────────────────────
-     │                                                       │
-     │                                                       │  8. 获取用户信息
-     │                                                       ├────────────────────>
-     │                                                       │                     OAuth
-     │                                                       │  9. 返回用户信息
-     │                                                       │<────────────────────
-     │                                                       │
-     │  10. 登录成功,建立会话                               │
-     │<──────────────────────────────────────────────────────┤
-     │                                                       │
-```
-
-## OAuth 认证中心信息
-
-### 基础配置
-
-- **OAuth 服务地址**: `http://192.168.92.61:8000`
-- **授权端点**: `http://192.168.92.61:8000/oauth/authorize`
-- **令牌端点**: `http://192.168.92.61:8000/oauth/token`
-- **用户信息端点**: `http://192.168.92.61:8000/oauth/userinfo`
-- **撤销端点**: `http://192.168.92.61:8000/oauth/revoke`
-
-### 应用配置(待提供)
-
-- **Client ID (应用Key)**: `待提供`
-- **Client Secret (应用密钥)**: `待提供`
-- **回调 URL**: `http://localhost:4200/auth/callback` (开发环境)
-- **回调 URL**: `http://192.168.92.61:8100/auth/callback` (生产环境)
-
-### 授权参数
-
-```
-response_type: code              # 授权码模式
-client_id: <YOUR_CLIENT_ID>      # 应用标识
-redirect_uri: <YOUR_CALLBACK>    # 回调地址
-scope: profile email             # 请求的权限范围
-state: <RANDOM_STRING>           # 防CSRF攻击的随机字符串
-```
-
-## 后端实现方案
-
-### 1. 环境配置
-
-在 `backend/.env` 中添加 OAuth 配置:
-
-```env
-# OAuth 2.0 配置
-OAUTH_ENABLED=true
-OAUTH_BASE_URL=http://192.168.92.61:8000
-OAUTH_CLIENT_ID=<待提供的Client ID>
-OAUTH_CLIENT_SECRET=<待提供的Client Secret>
-OAUTH_REDIRECT_URI=http://localhost:4200/auth/callback
-OAUTH_SCOPE=profile email
-```
-
-### 2. 更新配置模块
-
-修改 `backend/config.py`:
-
-```python
-from pydantic_settings import BaseSettings
-from typing import Optional
-
-class Settings(BaseSettings):
-    # ... 现有配置 ...
-    
-    # OAuth 2.0 配置
-    OAUTH_ENABLED: bool = False
-    OAUTH_BASE_URL: str = "http://192.168.92.61:8000"
-    OAUTH_CLIENT_ID: str = ""
-    OAUTH_CLIENT_SECRET: str = ""
-    OAUTH_REDIRECT_URI: str = "http://localhost:4200/auth/callback"
-    OAUTH_SCOPE: str = "profile email"
-    
-    class Config:
-        env_file = ".env"
-        case_sensitive = True
-
-settings = Settings()
-```
-
-### 3. 创建 OAuth 服务
-
-创建 `backend/services/oauth_service.py`:
-
-```python
-"""
-OAuth 2.0 认证服务
-"""
-import httpx
-import secrets
-from typing import Dict, Any, Optional
-from backend.config import settings
-from backend.models import User
-from backend.database import get_db_connection
-from datetime import datetime
-
-class OAuthService:
-    """OAuth 认证服务"""
-    
-    @staticmethod
-    def generate_state() -> str:
-        """生成随机 state 参数"""
-        return secrets.token_urlsafe(32)
-    
-    @staticmethod
-    def get_authorization_url(state: str) -> str:
-        """
-        构建授权 URL
-        
-        Args:
-            state: 防CSRF的随机字符串
-            
-        Returns:
-            完整的授权URL
-        """
-        from urllib.parse import urlencode
-        
-        params = {
-            "response_type": "code",
-            "client_id": settings.OAUTH_CLIENT_ID,
-            "redirect_uri": settings.OAUTH_REDIRECT_URI,
-            "scope": settings.OAUTH_SCOPE,
-            "state": state
-        }
-        
-        return f"{settings.OAUTH_BASE_URL}/oauth/authorize?{urlencode(params)}"
-    
-    @staticmethod
-    async def exchange_code_for_token(code: str) -> Dict[str, Any]:
-        """
-        用授权码换取访问令牌
-        
-        Args:
-            code: 授权码
-            
-        Returns:
-            令牌信息字典
-        """
-        async with httpx.AsyncClient() as client:
-            response = await client.post(
-                f"{settings.OAUTH_BASE_URL}/oauth/token",
-                data={
-                    "grant_type": "authorization_code",
-                    "code": code,
-                    "redirect_uri": settings.OAUTH_REDIRECT_URI,
-                    "client_id": settings.OAUTH_CLIENT_ID,
-                    "client_secret": settings.OAUTH_CLIENT_SECRET
-                }
-            )
-            
-            if response.status_code != 200:
-                raise Exception(f"令牌交换失败: {response.text}")
-            
-            data = response.json()
-            
-            # 处理不同的响应格式
-            if "access_token" in data:
-                return data
-            elif data.get("code") == 0 and "data" in data:
-                return data["data"]
-            else:
-                raise Exception(f"无效的令牌响应格式: {data}")
-    
-    @staticmethod
-    async def get_user_info(access_token: str) -> Dict[str, Any]:
-        """
-        获取用户信息
-        
-        Args:
-            access_token: 访问令牌
-            
-        Returns:
-            用户信息字典
-        """
-        async with httpx.AsyncClient() as client:
-            response = await client.get(
-                f"{settings.OAUTH_BASE_URL}/oauth/userinfo",
-                headers={"Authorization": f"Bearer {access_token}"}
-            )
-            
-            if response.status_code != 200:
-                raise Exception(f"获取用户信息失败: {response.text}")
-            
-            data = response.json()
-            
-            # 处理不同的响应格式
-            if "sub" in data:
-                return data
-            elif data.get("code") == 0 and "data" in data:
-                return data["data"]
-            else:
-                raise Exception(f"无效的用户信息响应格式: {data}")
-    
-    @staticmethod
-    async def sync_user_from_oauth(oauth_user_info: Dict[str, Any]) -> User:
-        """
-        从 OAuth 用户信息同步到本地数据库
-        
-        Args:
-            oauth_user_info: OAuth 返回的用户信息
-            
-        Returns:
-            本地用户对象
-        """
-        with get_db_connection() as conn:
-            cursor = conn.cursor()
-            
-            # 提取

+ 0 - 331
OAUTH_LOGIN_TEST_GUIDE.md

@@ -1,331 +0,0 @@
-# OAuth 单点登录测试指南
-
-## 概述
-
-OAuth 2.0 单点登录已成功集成到标注平台!现在可以使用 SSO 认证中心进行登录。
-
-## 配置信息
-
-### 后端配置 (`backend/config.yaml`)
-
-```yaml
-oauth:
-  enabled: true
-  base_url: "http://192.168.92.61:8000"
-  client_id: "sRyfcQwNVoFimigzuuZxhqd36fPkVN5G"
-  client_secret: "96RuKb4obAn9bQ9i5NtINiKBMvF_9uuCR7eNzD9dWQMbOWZaV3P593-8yLOqzWRd"
-  redirect_uri: "http://localhost:4200/auth/callback"
-  scope: "profile email"
-```
-
-### OAuth 端点
-
-- **授权端点**: `http://192.168.92.61:8000/oauth/authorize`
-- **令牌端点**: `http://192.168.92.61:8000/oauth/token`
-- **用户信息端点**: `http://192.168.92.61:8000/oauth/userinfo`
-
-## 测试步骤
-
-### 1. 启动服务
-
-**后端**:
-```bash
-cd backend
-python main.py
-```
-
-**前端**:
-```bash
-cd web
-yarn nx serve lq_label
-```
-
-### 2. 访问登录页面
-
-打开浏览器访问: `http://localhost:4200/login`
-
-### 3. 使用 OAuth 登录
-
-1. 点击 **"使用 SSO 登录"** 按钮
-2. 浏览器会重定向到 OAuth 认证中心 (`http://192.168.92.61:8000`)
-3. 在 SSO 登录页面输入用户名和密码
-4. 授权后,浏览器会重定向回标注平台 (`http://localhost:4200/auth/callback`)
-5. 系统自动完成登录,跳转到首页
-
-### 4. 验证登录状态
-
-登录成功后,你应该能看到:
-- 右上角显示用户名和头像
-- 可以访问所有受保护的页面(项目、任务、标注等)
-- 用户菜单显示用户信息和"退出登录"按钮
-
-## OAuth 登录流程
-
-```
-1. 用户点击"使用 SSO 登录"
-   ↓
-2. 前端调用 /api/oauth/login 获取授权 URL 和 state
-   ↓
-3. 前端保存 state 到 sessionStorage
-   ↓
-4. 前端重定向到 OAuth 授权页面
-   ↓
-5. 用户在 SSO 登录并授权
-   ↓
-6. OAuth 重定向回 /auth/callback?code=xxx&state=yyy
-   ↓
-7. 前端验证 state 参数
-   ↓
-8. 前端调用 /api/oauth/callback 用 code 换取 token
-   ↓
-9. 后端用 code 向 OAuth 换取 access_token
-   ↓
-10. 后端用 access_token 获取用户信息
-   ↓
-11. 后端同步用户到本地数据库
-   ↓
-12. 后端生成本地 JWT tokens
-   ↓
-13. 前端保存 tokens 和用户信息
-   ↓
-14. 前端跳转到首页
-```
-
-## API 端点
-
-### 后端 OAuth API
-
-| 端点 | 方法 | 描述 |
-|------|------|------|
-| `/api/oauth/login` | GET | 获取授权 URL 和 state |
-| `/api/oauth/callback` | GET | 处理 OAuth 回调,换取 token |
-| `/api/oauth/status` | GET | 获取 OAuth 配置状态 |
-
-### 示例请求
-
-**获取授权 URL**:
-```bash
-curl http://localhost:8000/api/oauth/login
-```
-
-**响应**:
-```json
-{
-  "authorization_url": "http://192.168.92.61:8000/oauth/authorize?response_type=code&client_id=sRyfcQwNVoFimigzuuZxhqd36fPkVN5G&redirect_uri=http://localhost:4200/auth/callback&scope=profile+email&state=xxx",
-  "state": "xxx"
-}
-```
-
-**处理回调**:
-```bash
-curl "http://localhost:8000/api/oauth/callback?code=xxx&state=yyy"
-```
-
-**响应**:
-```json
-{
-  "access_token": "eyJ...",
-  "refresh_token": "eyJ...",
-  "token_type": "bearer",
-  "user": {
-    "id": "user_xxx",
-    "username": "testuser",
-    "email": "test@example.com",
-    "role": "annotator",
-    "created_at": "2024-01-22T..."
-  }
-}
-```
-
-## 用户同步逻辑
-
-### 首次登录
-
-1. OAuth 返回用户信息(包含 `sub` 或 `id` 字段)
-2. 系统检查本地数据库是否存在该 OAuth 用户
-3. 如果不存在,创建新用户记录:
-   - `oauth_provider`: "sso"
-   - `oauth_id`: OAuth 用户 ID
-   - `username`: OAuth 用户名
-   - `email`: OAuth 邮箱
-   - `role`: "annotator"(默认)
-   - `password_hash`: ""(OAuth 用户不需要密码)
-
-### 再次登录
-
-1. 系统根据 `oauth_provider` 和 `oauth_id` 查找用户
-2. 更新用户名和邮箱(如果有变化)
-3. 返回现有用户信息
-
-## 角色管理
-
-**当前实现**:
-- 所有 OAuth 用户默认角色为 `annotator`(标注员)
-- SSO 暂时未提供角色信息
-
-**未来扩展**:
-- 当 SSO 提供角色信息后,可以从 OAuth 用户信息中读取角色
-- 支持 `admin` 和 `annotator` 两种角色
-- 管理员可以删除项目和任务
-
-## 安全特性
-
-### State 参数验证
-
-- 前端生成随机 state 参数并保存到 sessionStorage
-- OAuth 回调时验证 state 参数是否匹配
-- 防止 CSRF 攻击
-
-### Token 安全
-
-- OAuth access_token 仅用于获取用户信息
-- 系统生成独立的 JWT tokens 用于后续 API 调用
-- JWT tokens 有过期时间(access: 15分钟,refresh: 7天)
-
-### 用户隔离
-
-- 每个 OAuth 用户在本地数据库有独立记录
-- 用户只能访问自己的标注数据
-- 管理员可以访问所有数据
-
-## 故障排除
-
-### 问题 1: "无效的客户端ID" 错误
-
-**原因**: OAuth 服务器上的应用 ID 配置不正确
-
-**解决方案**:
-- 确认 `backend/config.yaml` 中的 `client_id` 是正确的
-- 当前正确的 ID:`sRyfcQwNVoFimigzuuZxhqd36fPkVN5G`
-- 确认 OAuth 服务器上已创建该应用
-
-### 问题 2: "无效的重定向URI" 错误
-
-**原因**: 回调 URL 未在 OAuth 服务器上配置
-
-**解决方案**:
-- 在 OAuth 服务器上添加回调 URL:`http://localhost:4200/auth/callback`
-- 确保 URL 完全匹配(包括协议、端口、路径)
-
-### 问题 3: "授权码已被使用" 错误
-
-**原因**: React StrictMode 在开发环境下会导致组件渲染两次,授权码被使用两次
-
-**解决方案**:
-- 已在 `oauth-callback.tsx` 中添加防重复调用逻辑(使用 `isProcessing` 状态)
-- 生产环境不会有此问题(StrictMode 仅在开发环境启用)
-
-### 问题 4: Client ID 和 Secret 配置错误
-
-**症状**: 配置文件中 `client_id` 和 `client_secret` 的值搞反了
-
-**正确配置**:
-```yaml
-oauth:
-  client_id: "sRyfcQwNVoFimigzuuZxhqd36fPkVN5G"
-  client_secret: "96RuKb4obAn9bQ9i5NtINiKBMvF_9uuCR7eNzD9dWQMbOWZaV3P593-8yLOqzWRd"
-```
-
-### 问题 5: 点击"使用 SSO 登录"没有反应
-
-**检查**:
-1. 打开浏览器控制台查看错误
-2. 确认后端 OAuth 配置正确
-3. 确认 OAuth 服务可访问
-
-### 问题 6: 重定向到 OAuth 后无法返回
-
-**检查**:
-1. 确认 `redirect_uri` 配置正确
-2. 确认 OAuth 中心配置了正确的回调 URL
-3. 检查浏览器控制台错误
-
-### 问题 7: 回调后显示"登录失败"
-
-**检查**:
-1. 查看浏览器控制台错误信息
-2. 查看后端日志
-3. 确认 OAuth 令牌交换成功
-4. 确认用户信息获取成功
-
-### 问题 8: State 参数不匹配
-
-**原因**: sessionStorage 被清除或浏览器不支持
-
-**解决方案**:
-1. 不要在登录过程中清除浏览器缓存
-2. 确保浏览器支持 sessionStorage
-3. 检查是否有浏览器扩展干扰
-
-## 测试账号
-
-请使用 SSO 认证中心提供的测试账号进行测试。
-
-## 开发调试
-
-### 查看 OAuth 配置状态
-
-```bash
-curl http://localhost:8000/api/oauth/status
-```
-
-### 查看用户数据库
-
-```bash
-cd backend
-sqlite3 annotation_platform.db
-SELECT * FROM users WHERE oauth_provider = 'sso';
-```
-
-### 清除 OAuth 用户
-
-```bash
-cd backend
-sqlite3 annotation_platform.db
-DELETE FROM users WHERE oauth_provider = 'sso';
-```
-
-## 生产环境配置
-
-### 更新回调 URL
-
-修改 `backend/config.yaml`:
-
-```yaml
-oauth:
-  redirect_uri: "http://your-production-domain.com/auth/callback"
-```
-
-### 更新前端 CORS
-
-修改 `backend/main.py`:
-
-```python
-app.add_middleware(
-    CORSMiddleware,
-    allow_origins=[
-        "http://your-production-domain.com",
-    ],
-    ...
-)
-```
-
-## 总结
-
-OAuth 单点登录已成功集成!主要特性:
-
-- ✅ 支持 OAuth 2.0 授权码模式
-- ✅ 自动用户同步和创建
-- ✅ State 参数防 CSRF 攻击
-- ✅ 独立的 JWT token 管理
-- ✅ 用户角色支持(默认 annotator)
-- ✅ 与现有认证系统兼容
-- ✅ 支持传统用户名密码登录
-
-现在可以使用 SSO 登录标注平台了!🎉
-
----
-
-**实现日期**: 2024-01-22  
-**状态**: ✅ 完成  
-**文档版本**: 1.0

+ 0 - 245
QUICK_START.md

@@ -1,245 +0,0 @@
-# 标注平台快速开始指南
-
-## 🚀 快速开始
-
-### 1. 启动服务
-
-**启动后端服务器:**
-```bash
-cd backend
-python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000
-```
-
-**启动前端服务器:**
-```bash
-cd web
-yarn nx serve lq_label
-```
-
-### 2. 初始化示例数据
-
-```bash
-cd backend
-python init_sample_data.py
-```
-
-这将创建 3 个示例项目和 6 个示例任务:
-
-1. **情感分析标注项目** - 3 个文本分类任务
-2. **命名实体识别项目** - 2 个 NER 任务
-3. **文本高亮标注项目** - 1 个文本高亮任务
-
-### 3. 访问应用
-
-打开浏览器访问:http://localhost:4200
-
-## 📝 标注流程演示
-
-### 步骤 1:查看项目列表
-
-1. 访问 http://localhost:4200/projects
-2. 你会看到 3 个示例项目
-
-### 步骤 2:查看项目详情
-
-1. 点击"情感分析标注项目"
-2. 查看项目信息和关联的任务列表
-3. 你会看到 3 个待标注的任务
-
-### 步骤 3:开始标注
-
-1. 在任务列表中找到"文本分类任务-1"
-2. 点击"开始标注"按钮
-3. LabelStudio 编辑器将加载
-
-### 步骤 4:进行标注
-
-**文本分类示例:**
-- 文本:这家餐厅的服务态度非常好,菜品也很美味,环境优雅,强烈推荐!
-- 操作:选择"正面"选项
-- 点击"保存"按钮
-
-**命名实体识别示例:**
-- 文本:2024年1月15日,张三在北京大学参加了人工智能研讨会。
-- 操作:
-  1. 选中"2024年1月15日",标记为"时间"
-  2. 选中"张三",标记为"人名"
-  3. 选中"北京大学",标记为"机构名"
-- 点击"保存"按钮
-
-### 步骤 5:查看结果
-
-1. 返回任务列表
-2. 查看任务状态已更新为"进行中"
-3. 进度已增加
-
-## 🎯 示例项目详情
-
-### 1. 情感分析标注项目
-
-**目标:** 对用户评论进行情感分类
-
-**标注类型:** 单选分类
-
-**类别:**
-- 正面
-- 负面
-- 中性
-
-**示例任务:**
-
-| 任务名称 | 文本内容 | 预期标注 |
-|---------|---------|---------|
-| 文本分类任务-1 | 这家餐厅的服务态度非常好... | 正面 |
-| 文本分类任务-2 | 产品质量太差了... | 负面 |
-| 文本分类任务-3 | 这款手机性能一般... | 中性 |
-
-### 2. 命名实体识别项目
-
-**目标:** 识别文本中的实体
-
-**标注类型:** 文本高亮标注
-
-**实体类型:**
-- 人名(红色)
-- 地名(蓝色)
-- 机构名(绿色)
-- 时间(橙色)
-
-**示例任务:**
-
-| 任务名称 | 文本内容 | 预期实体 |
-|---------|---------|---------|
-| 命名实体识别任务-1 | 2024年1月15日,张三在北京大学... | 时间、人名、机构名 |
-| 命名实体识别任务-2 | 李明是清华大学的教授... | 人名、机构名 |
-
-### 3. 文本高亮标注项目
-
-**目标:** 标记文本中的关键信息
-
-**标注类型:** 文本高亮标注
-
-**标签类型:**
-- 重要信息(黄色)
-- 关键词(浅蓝色)
-- 问题(粉色)
-
-## 🔧 测试工具
-
-### 检查示例数据
-
-```bash
-cd backend
-python test_annotation.py check
-```
-
-### 测试完整工作流程
-
-```bash
-cd backend
-python test_annotation.py
-```
-
-这将自动测试:
-1. 创建项目
-2. 创建任务
-3. 创建标注
-4. 查询标注
-5. 更新任务状态
-6. 清理测试数据
-
-## 📊 数据库查询
-
-### 查看项目数量
-
-```bash
-cd backend
-python -c "import sqlite3; conn = sqlite3.connect('annotation_platform.db'); cursor = conn.cursor(); cursor.execute('SELECT COUNT(*) FROM projects'); print(f'Projects: {cursor.fetchone()[0]}'); conn.close()"
-```
-
-### 查看任务数量
-
-```bash
-cd backend
-python -c "import sqlite3; conn = sqlite3.connect('annotation_platform.db'); cursor = conn.cursor(); cursor.execute('SELECT COUNT(*) FROM tasks'); print(f'Tasks: {cursor.fetchone()[0]}'); conn.close()"
-```
-
-### 查看标注数量
-
-```bash
-cd backend
-python -c "import sqlite3; conn = sqlite3.connect('annotation_platform.db'); cursor = conn.cursor(); cursor.execute('SELECT COUNT(*) FROM annotations'); print(f'Annotations: {cursor.fetchone()[0]}'); conn.close()"
-```
-
-## 🎨 LabelStudio 编辑器使用技巧
-
-### 文本分类
-
-1. 阅读文本内容
-2. 点击对应的分类选项
-3. 点击"保存"按钮
-
-### 命名实体识别
-
-1. 用鼠标选中要标注的文本
-2. 在弹出的标签列表中选择实体类型
-3. 重复步骤 1-2 标注所有实体
-4. 点击"保存"按钮
-
-### 文本高亮
-
-1. 用鼠标选中要标注的文本
-2. 在弹出的标签列表中选择标签类型
-3. 重复步骤 1-2 标注所有需要的文本
-4. 点击"保存"按钮
-
-## ❓ 常见问题
-
-### Q: 编辑器无法加载?
-
-**A:** 检查以下几点:
-1. 前端服务器是否正在运行(等待编译完成,看到 "webpack compiled" 消息)
-2. 浏览器控制台是否有 JavaScript 错误(不是 webpack 警告)
-3. 项目配置是否正确(XML 格式)
-4. 等待编辑器初始化完成(可能需要几秒钟)
-
-### Q: 前端编译有很多警告?
-
-**A:** 这是正常的:
-1. Sass 弃用警告(@import 规则)是来自 UI 库的,不影响功能
-2. Source map 警告也不影响功能
-3. 只要看到 "webpack compiled with X warnings" 就表示编译成功
-4. 首次编译可能需要 30-60 秒,请耐心等待
-
-### Q: 保存标注时提示错误?
-
-**A:** 检查以下几点:
-1. 是否完成了标注(标注结果不能为空)
-2. 后端 API 是否正常工作
-3. 查看浏览器控制台的错误信息
-
-### Q: 如何清理示例数据?
-
-**A:** 删除数据库文件:
-```bash
-cd backend
-rm annotation_platform.db
-```
-然后重启后端服务器。
-
-## 📚 更多资源
-
-- **LabelStudio 文档:** https://labelstud.io/guide/
-- **LabelStudio 配置标签:** https://labelstud.io/tags/
-- **项目 GitHub:** https://github.com/HumanSignal/label-studio
-
-## 🎉 下一步
-
-现在你已经了解了基本的标注流程,可以:
-
-1. 创建自己的项目和任务
-2. 自定义标注配置
-3. 邀请团队成员进行标注
-4. 导出标注结果进行分析
-
-祝标注愉快!🚀

+ 0 - 157
TOAST_CUSTOM_IMPLEMENTATION.md

@@ -1,157 +0,0 @@
-# Toast 自定义实现文档
-
-## 概述
-
-由于 UI 库的 Toast 组件使用 CSS Modules,类名被哈希化(如 `toast_toast__Zvlxi`),导致样式覆盖困难。因此,我们创建了一个完全自定义的 Toast 组件,不依赖 UI 库。
-
-## 实现文件
-
-### 1. Toast 组件
-- **位置**: `web/apps/lq_label/src/components/toast/`
-- **文件**:
-  - `toast.tsx` - Toast 组件实现
-  - `toast.module.scss` - Toast 样式
-  - `index.ts` - 导出文件
-
-### 2. Toast 服务
-- **位置**: `web/apps/lq_label/src/services/toast.ts`
-- **功能**: 提供全局 Toast 通知服务
-
-### 3. Toast 容器
-- **位置**: `web/apps/lq_label/src/components/toast-container/toast-container.tsx`
-- **功能**: 连接 Toast 服务和 Toast 组件
-
-## Toast 类型
-
-```typescript
-export enum ToastType {
-  success = 'success',  // 成功提示 - 绿色
-  error = 'error',      // 错误提示 - 红色
-  info = 'info',        // 信息提示 - 黑色
-  warning = 'warning',  // 警告提示 - 橙色
-}
-```
-
-## 使用方法
-
-```typescript
-import { toast } from '../../services/toast';
-
-// 成功提示
-toast.success('操作成功!');
-toast.success('保存成功', '成功', 3000);
-
-// 错误提示
-toast.error('操作失败!');
-toast.error('保存失败,请重试', '错误', 5000);
-
-// 信息提示
-toast.info('这是一条信息');
-toast.info('系统通知', '提示', 4000);
-
-// 警告提示
-toast.warning('请注意!');
-toast.warning('数据可能不完整', '警告', 4000);
-```
-
-## 样式特性
-
-### 1. 设计令牌
-所有样式使用项目的设计令牌(Design Tokens):
-- `--color-positive-*` - 成功颜色
-- `--color-negative-*` - 错误颜色
-- `--color-neutral-inverted-*` - 信息颜色
-- `--color-warning-*` - 警告颜色
-- `--spacing-*` - 间距
-- `--corner-radius-*` - 圆角
-- `--font-size-*` - 字体大小
-
-### 2. 视觉效果
-- 最小高度: 56px
-- 圆角边框
-- 阴影和模糊效果(backdrop-filter)
-- 错误/警告类型左侧粗边框(4px)
-- 平滑动画(cubic-bezier)
-
-### 3. 响应式设计
-- 桌面端: 最大宽度 420px,居中显示
-- 移动端: 宽度自适应,减小内边距
-
-### 4. 交互
-- 关闭按钮: 点击关闭
-- 自动关闭: 可配置持续时间
-- 退出动画: 平滑淡出
-
-## 样式示例
-
-### 成功提示
-- 背景: 白色
-- 标题: 绿色(kale-700)
-- 消息: 深灰色(sand-800)
-- 左侧粗边框: 深绿色(kale-800)
-- 边框: 绿色(kale-700)
-
-### 错误提示
-- 背景: 白色
-- 标题: 红色(persimmon-700)
-- 消息: 深灰色(sand-800)
-- 左侧粗边框: 深红色(persimmon-800)
-- 边框: 红色(persimmon-700)
-
-### 信息提示
-- 背景: 深灰色(sand-900)
-- 文字: 白色
-- 无粗边框
-
-### 警告提示
-- 背景: 白色
-- 标题: 橙色(canteloupe-700)
-- 消息: 深灰色(sand-800)
-- 左侧粗边框: 深橙色(canteloupe-800)
-- 边框: 橙色(canteloupe-700)
-
-## 优势
-
-1. **完全可控**: 不受 UI 库 CSS Modules 限制
-2. **样式一致**: 使用项目设计令牌
-3. **性能优化**: 轻量级实现,无额外依赖
-4. **易于维护**: 代码清晰,结构简单
-5. **响应式**: 适配桌面和移动端
-
-## 清理的文件
-
-以下文件已被删除或替换:
-- `web/apps/lq_label/src/components/toast-container/toast-container.module.scss` - 已删除(旧的样式覆盖文件)
-
-## 注意事项
-
-1. **不要使用 UI 库的 Toast**: 已完全移除对 `@humansignal/ui` Toast 组件的依赖
-2. **使用自定义 Toast**: 所有 Toast 通知都应使用 `toast` 服务
-3. **设计令牌**: 确保使用项目中实际存在的 CSS 变量
-
-## 测试
-
-重启前端服务器后,可以通过以下方式测试:
-
-```typescript
-// 在任何组件中
-import { toast } from '../../services/toast';
-
-// 测试不同类型的 Toast
-toast.success('这是成功提示');
-toast.error('这是错误提示');
-toast.info('这是信息提示');
-toast.warning('这是警告提示');
-```
-
-## 相关文件
-
-- `web/apps/lq_label/src/components/toast/toast.tsx`
-- `web/apps/lq_label/src/components/toast/toast.module.scss`
-- `web/apps/lq_label/src/services/toast.ts`
-- `web/apps/lq_label/src/components/toast-container/toast-container.tsx`
-- `web/apps/lq_label/src/app/app.tsx` (ToastContainer 已集成)
-
-## 更新日期
-
-2025-01-23

+ 10 - 2
backend/config.docker.yaml

@@ -25,8 +25,16 @@ oauth:
 
 # 数据库配置
 database:
-  # Docker 环境中使用挂载的数据目录
-  path: "/app/data/annotation_platform.db"
+  type: "mysql"  # sqlite 或 mysql
+  path: "/app/data/annotation_platform.db"  # SQLite 路径
+  
+  # MySQL 配置
+  mysql:
+    host: "192.168.92.61"
+    port: 13306
+    user: "root"
+    password: "lq123"
+    database: "annotation_platform"
 
 # 服务器配置
 server:

+ 9 - 0
backend/config.py

@@ -34,8 +34,17 @@ class Settings:
         
         # Database Settings
         db_config = config.get('database', {})
+        self.DATABASE_TYPE = db_config.get('type', 'sqlite')
         self.DATABASE_PATH = db_config.get('path', 'annotation_platform.db')
         
+        # MySQL Settings
+        mysql_config = db_config.get('mysql', {})
+        self.MYSQL_HOST = mysql_config.get('host', 'localhost')
+        self.MYSQL_PORT = mysql_config.get('port', 3306)
+        self.MYSQL_USER = mysql_config.get('user', 'root')
+        self.MYSQL_PASSWORD = mysql_config.get('password', '')
+        self.MYSQL_DATABASE = mysql_config.get('database', 'annotation_platform')
+        
         # OAuth Settings
         oauth_config = config.get('oauth', {})
         self.OAUTH_ENABLED = oauth_config.get('enabled', False)

+ 10 - 1
backend/config.yaml

@@ -24,7 +24,16 @@ oauth:
 
 # 数据库配置
 database:
-  path: "annotation_platform.db"
+  type: "mysql"  # sqlite 或 mysql
+  path: "annotation_platform.db"  # SQLite 文件路径(仅 type=sqlite 时使用)
+  
+  # MySQL 配置(仅 type=mysql 时使用)
+  mysql:
+    host: "192.168.92.61"
+    port: 13306
+    user: "root"
+    password: "lq123"
+    database: "annotation_platform"
 
 # 服务器配置
 server:

+ 248 - 34
backend/database.py

@@ -1,45 +1,266 @@
 """
 Database connection and initialization module.
-Manages SQLite database connection and table creation.
+Supports both SQLite and MySQL databases.
 """
-import sqlite3
 import os
+import logging
 from contextlib import contextmanager
-from typing import Generator
+from typing import Generator, Any, Optional
+from config import settings
 
-# Database file path
-DB_PATH = os.getenv("DATABASE_PATH", "annotation_platform.db")
+logger = logging.getLogger(__name__)
+
+
+class DatabaseConfig:
+    """Database configuration holder."""
+    
+    def __init__(self):
+        self.db_type = getattr(settings, 'DATABASE_TYPE', 'sqlite')
+        
+        if self.db_type == 'mysql':
+            self.host = getattr(settings, 'MYSQL_HOST', 'localhost')
+            self.port = getattr(settings, 'MYSQL_PORT', 3306)
+            self.user = getattr(settings, 'MYSQL_USER', 'root')
+            self.password = getattr(settings, 'MYSQL_PASSWORD', '')
+            self.database = getattr(settings, 'MYSQL_DATABASE', 'annotation_platform')
+        else:
+            self.db_path = getattr(settings, 'DATABASE_PATH', 'annotation_platform.db')
+
+
+db_config = DatabaseConfig()
+
+
+def _get_mysql_connection():
+    """Get MySQL connection."""
+    import pymysql
+    conn = pymysql.connect(
+        host=db_config.host,
+        port=db_config.port,
+        user=db_config.user,
+        password=db_config.password,
+        database=db_config.database,
+        charset='utf8mb4',
+        cursorclass=pymysql.cursors.DictCursor,
+        autocommit=False
+    )
+    return conn
+
+
+def _get_sqlite_connection():
+    """Get SQLite connection."""
+    import sqlite3
+    conn = sqlite3.connect(db_config.db_path)
+    conn.row_factory = sqlite3.Row
+    conn.execute("PRAGMA foreign_keys = ON")
+    return conn
+
+
+class RowWrapper:
+    """Wrapper to provide consistent row access for both SQLite and MySQL."""
+    
+    def __init__(self, row, db_type: str):
+        self._row = row
+        self._db_type = db_type
+    
+    def __getitem__(self, key):
+        if self._db_type == 'mysql':
+            return self._row[key]
+        else:
+            return self._row[key]
+    
+    def keys(self):
+        if self._db_type == 'mysql':
+            return self._row.keys()
+        else:
+            return self._row.keys()
+
+
+class CursorWrapper:
+    """Wrapper to provide consistent cursor interface for both databases."""
+    
+    def __init__(self, cursor, db_type: str):
+        self._cursor = cursor
+        self._db_type = db_type
+    
+    def execute(self, sql: str, params: tuple = None):
+        """Execute SQL with parameter conversion."""
+        if self._db_type == 'mysql':
+            # Convert ? placeholders to %s for MySQL
+            sql = sql.replace('?', '%s')
+        
+        if params:
+            self._cursor.execute(sql, params)
+        else:
+            self._cursor.execute(sql)
+        return self
+    
+    def fetchone(self) -> Optional[RowWrapper]:
+        row = self._cursor.fetchone()
+        if row is None:
+            return None
+        return RowWrapper(row, self._db_type)
+    
+    def fetchall(self) -> list:
+        rows = self._cursor.fetchall()
+        return [RowWrapper(row, self._db_type) for row in rows]
+    
+    @property
+    def lastrowid(self):
+        return self._cursor.lastrowid
+    
+    @property
+    def rowcount(self):
+        return self._cursor.rowcount
+
+
+class ConnectionWrapper:
+    """Wrapper to provide consistent connection interface."""
+    
+    def __init__(self, conn, db_type: str):
+        self._conn = conn
+        self._db_type = db_type
+    
+    def cursor(self) -> CursorWrapper:
+        return CursorWrapper(self._conn.cursor(), self._db_type)
+    
+    def commit(self):
+        self._conn.commit()
+    
+    def rollback(self):
+        self._conn.rollback()
+    
+    def close(self):
+        self._conn.close()
+    
+    def execute(self, sql: str, params: tuple = None):
+        cursor = self.cursor()
+        cursor.execute(sql, params)
+        return cursor
 
 
 @contextmanager
-def get_db_connection() -> Generator[sqlite3.Connection, None, None]:
+def get_db_connection() -> Generator[ConnectionWrapper, None, None]:
     """
     Context manager for database connections.
     Ensures proper connection cleanup.
     """
-    conn = sqlite3.connect(DB_PATH)
-    conn.row_factory = sqlite3.Row  # Enable column access by name
-    conn.execute("PRAGMA foreign_keys = ON")  # Enable foreign key constraints
+    if db_config.db_type == 'mysql':
+        conn = _get_mysql_connection()
+    else:
+        conn = _get_sqlite_connection()
+    
+    wrapped = ConnectionWrapper(conn, db_config.db_type)
+    
     try:
-        yield conn
-        conn.commit()
+        yield wrapped
+        wrapped.commit()
     except Exception:
-        conn.rollback()
+        wrapped.rollback()
         raise
     finally:
-        conn.close()
+        wrapped.close()
 
 
 def init_database() -> None:
     """
     Initialize database and create tables if they don't exist.
-    Creates projects, tasks, annotations, and users tables with proper relationships.
     """
+    if db_config.db_type == 'mysql':
+        _init_mysql_database()
+    else:
+        _init_sqlite_database()
+
+
+def _init_mysql_database() -> None:
+    """Initialize MySQL database tables."""
+    import pymysql
+    
+    # First, create database if not exists
+    conn = pymysql.connect(
+        host=db_config.host,
+        port=db_config.port,
+        user=db_config.user,
+        password=db_config.password,
+        charset='utf8mb4'
+    )
+    try:
+        with conn.cursor() as cursor:
+            cursor.execute(f"CREATE DATABASE IF NOT EXISTS `{db_config.database}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
+        conn.commit()
+    finally:
+        conn.close()
+    
+    # Now create tables
     with get_db_connection() as conn:
         cursor = conn.cursor()
         
-        # Enable foreign key constraints
-        cursor.execute("PRAGMA foreign_keys = ON")
+        # Create users table
+        cursor.execute("""
+            CREATE TABLE IF NOT EXISTS users (
+                id VARCHAR(36) PRIMARY KEY,
+                username VARCHAR(255) NOT NULL UNIQUE,
+                email VARCHAR(255) NOT NULL UNIQUE,
+                password_hash VARCHAR(255) NOT NULL,
+                role VARCHAR(50) NOT NULL DEFAULT 'annotator',
+                oauth_provider VARCHAR(50),
+                oauth_id VARCHAR(255),
+                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+                INDEX idx_users_username (username),
+                INDEX idx_users_email (email),
+                INDEX idx_users_oauth (oauth_provider, oauth_id)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+        """)
+        
+        # Create projects table
+        cursor.execute("""
+            CREATE TABLE IF NOT EXISTS projects (
+                id VARCHAR(36) PRIMARY KEY,
+                name VARCHAR(255) NOT NULL,
+                description TEXT,
+                config TEXT NOT NULL,
+                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+        """)
+        
+        # Create tasks table
+        cursor.execute("""
+            CREATE TABLE IF NOT EXISTS tasks (
+                id VARCHAR(36) PRIMARY KEY,
+                project_id VARCHAR(36) NOT NULL,
+                name VARCHAR(255) NOT NULL,
+                data LONGTEXT NOT NULL,
+                status VARCHAR(50) DEFAULT 'pending',
+                assigned_to VARCHAR(36),
+                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
+                INDEX idx_tasks_project (project_id),
+                INDEX idx_tasks_status (status)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+        """)
+        
+        # Create annotations table
+        cursor.execute("""
+            CREATE TABLE IF NOT EXISTS annotations (
+                id VARCHAR(36) PRIMARY KEY,
+                task_id VARCHAR(36) NOT NULL,
+                user_id VARCHAR(36) NOT NULL,
+                result LONGTEXT NOT NULL,
+                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+                FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+                INDEX idx_annotations_task (task_id),
+                INDEX idx_annotations_user (user_id)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+        """)
+        
+        logger.info("MySQL 数据库初始化完成")
+
+
+def _init_sqlite_database() -> None:
+    """Initialize SQLite database tables."""
+    with get_db_connection() as conn:
+        cursor = conn.cursor()
         
         # Create users table
         cursor.execute("""
@@ -56,17 +277,9 @@ def init_database() -> None:
             )
         """)
         
-        # Create indexes for users table
-        cursor.execute("""
-            CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)
-        """)
-        cursor.execute("""
-            CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)
-        """)
-        cursor.execute("""
-            CREATE INDEX IF NOT EXISTS idx_users_oauth 
-            ON users(oauth_provider, oauth_id)
-        """)
+        cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)")
+        cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)")
+        cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_oauth ON users(oauth_provider, oauth_id)")
         
         # Create projects table
         cursor.execute("""
@@ -106,15 +319,16 @@ def init_database() -> None:
             )
         """)
         
-        conn.commit()
+        logger.info("SQLite 数据库初始化完成")
 
 
-def get_db() -> sqlite3.Connection:
+def get_db():
     """
-    Get a database connection.
+    Get a database connection (legacy support).
     Note: Caller is responsible for closing the connection.
     """
-    conn = sqlite3.connect(DB_PATH)
-    conn.row_factory = sqlite3.Row
-    conn.execute("PRAGMA foreign_keys = ON")
-    return conn
+    if db_config.db_type == 'mysql':
+        conn = _get_mysql_connection()
+    else:
+        conn = _get_sqlite_connection()
+    return ConnectionWrapper(conn, db_config.db_type)

+ 2 - 0
backend/requirements.txt

@@ -14,3 +14,5 @@ hypothesis==6.92.1
 faker==20.1.0
 requests==2.31.0
 PyYAML==6.0.1
+PyMySQL==1.1.0
+cryptography==42.0.2

+ 40 - 192
backend/routers/task.py

@@ -16,6 +16,18 @@ router = APIRouter(
 )
 
 
+def calculate_progress(data_str: str, annotation_count: int) -> float:
+    """计算任务进度"""
+    try:
+        data = json.loads(data_str) if isinstance(data_str, str) else data_str
+        items = data.get('items', [])
+        if not items:
+            return 0.0
+        return min(annotation_count / len(items), 1.0)
+    except:
+        return 0.0
+
+
 @router.get("", response_model=List[TaskResponse])
 async def list_tasks(
     request: Request,
@@ -25,16 +37,6 @@ async def list_tasks(
 ):
     """
     List all tasks with optional filters.
-    
-    Args:
-        request: FastAPI Request object (contains user info)
-        project_id: Optional project ID filter
-        status_filter: Optional status filter (pending, in_progress, completed)
-        assigned_to: Optional assigned user filter
-    
-    Returns:
-        List of tasks matching the filters
-        
     Requires authentication.
     """
     with get_db_connection() as conn:
@@ -50,13 +52,7 @@ async def list_tasks(
                 t.status,
                 t.assigned_to,
                 t.created_at,
-                COALESCE(
-                    CAST(COUNT(a.id) AS FLOAT) / NULLIF(
-                        (SELECT COUNT(*) FROM json_each(t.data, '$.items')), 
-                        0
-                    ),
-                    0.0
-                ) as progress
+                COUNT(a.id) as annotation_count
             FROM tasks t
             LEFT JOIN annotations a ON t.id = a.task_id
             WHERE 1=1
@@ -75,15 +71,15 @@ async def list_tasks(
             query += " AND t.assigned_to = ?"
             params.append(assigned_to)
         
-        query += " GROUP BY t.id ORDER BY t.created_at DESC"
+        query += " GROUP BY t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at ORDER BY t.created_at DESC"
         
-        cursor.execute(query, params)
+        cursor.execute(query, tuple(params))
         rows = cursor.fetchall()
         
         tasks = []
         for row in rows:
-            # Parse JSON data
             data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
+            progress = calculate_progress(row["data"], row["annotation_count"])
             
             tasks.append(TaskResponse(
                 id=row["id"],
@@ -93,7 +89,7 @@ async def list_tasks(
                 status=row["status"],
                 assigned_to=row["assigned_to"],
                 created_at=row["created_at"],
-                progress=row["progress"]
+                progress=progress
             ))
         
         return tasks
@@ -103,27 +99,10 @@ async def list_tasks(
 async def create_task(request: Request, task: TaskCreate):
     """
     Create a new task.
-    
-    Args:
-        request: FastAPI Request object (contains user info)
-        task: Task creation data
-    
-    Returns:
-        Created task with generated ID
-    
-    Raises:
-        HTTPException: 404 if project not found
-        
     Requires authentication.
-    Note: If assigned_to is not provided, the task will be assigned to the current user.
     """
-    # Generate unique ID
     task_id = f"task_{uuid.uuid4().hex[:12]}"
-    
-    # Get current user
     user = request.state.user
-    
-    # Use provided assigned_to or default to current user
     assigned_to = task.assigned_to if task.assigned_to else user["id"]
     
     with get_db_connection() as conn:
@@ -137,31 +116,19 @@ async def create_task(request: Request, task: TaskCreate):
                 detail=f"Project with id '{task.project_id}' not found"
             )
         
-        # Serialize data to JSON
         data_json = json.dumps(task.data)
         
-        # Insert new task
         cursor.execute("""
             INSERT INTO tasks (id, project_id, name, data, status, assigned_to)
             VALUES (?, ?, ?, ?, 'pending', ?)
-        """, (
-            task_id,
-            task.project_id,
-            task.name,
-            data_json,
-            assigned_to
-        ))
-        
-        # Fetch the created task
+        """, (task_id, task.project_id, task.name, data_json, assigned_to))
+        
         cursor.execute("""
             SELECT id, project_id, name, data, status, assigned_to, created_at
-            FROM tasks
-            WHERE id = ?
+            FROM tasks WHERE id = ?
         """, (task_id,))
         
         row = cursor.fetchone()
-        
-        # Parse JSON data
         data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
         
         return TaskResponse(
@@ -180,23 +147,11 @@ async def create_task(request: Request, task: TaskCreate):
 async def get_task(request: Request, task_id: str):
     """
     Get task by ID.
-    
-    Args:
-        request: FastAPI Request object (contains user info)
-        task_id: Task unique identifier
-    
-    Returns:
-        Task details with progress
-    
-    Raises:
-        HTTPException: 404 if task not found
-        
     Requires authentication.
     """
     with get_db_connection() as conn:
         cursor = conn.cursor()
         
-        # Get task with progress
         cursor.execute("""
             SELECT 
                 t.id,
@@ -206,17 +161,11 @@ async def get_task(request: Request, task_id: str):
                 t.status,
                 t.assigned_to,
                 t.created_at,
-                COALESCE(
-                    CAST(COUNT(a.id) AS FLOAT) / NULLIF(
-                        (SELECT COUNT(*) FROM json_each(t.data, '$.items')), 
-                        0
-                    ),
-                    0.0
-                ) as progress
+                COUNT(a.id) as annotation_count
             FROM tasks t
             LEFT JOIN annotations a ON t.id = a.task_id
             WHERE t.id = ?
-            GROUP BY t.id
+            GROUP BY t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at
         """, (task_id,))
         
         row = cursor.fetchone()
@@ -227,8 +176,8 @@ async def get_task(request: Request, task_id: str):
                 detail=f"Task with id '{task_id}' not found"
             )
         
-        # Parse JSON data
         data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
+        progress = calculate_progress(row["data"], row["annotation_count"])
         
         return TaskResponse(
             id=row["id"],
@@ -238,7 +187,7 @@ async def get_task(request: Request, task_id: str):
             status=row["status"],
             assigned_to=row["assigned_to"],
             created_at=row["created_at"],
-            progress=row["progress"]
+            progress=progress
         )
 
 
@@ -246,24 +195,11 @@ async def get_task(request: Request, task_id: str):
 async def update_task(request: Request, task_id: str, task: TaskUpdate):
     """
     Update an existing task.
-    
-    Args:
-        request: FastAPI Request object (contains user info)
-        task_id: Task unique identifier
-        task: Task update data
-    
-    Returns:
-        Updated task details
-    
-    Raises:
-        HTTPException: 404 if task not found
-        
     Requires authentication.
     """
     with get_db_connection() as conn:
         cursor = conn.cursor()
         
-        # Check if task exists
         cursor.execute("SELECT id FROM tasks WHERE id = ?", (task_id,))
         if not cursor.fetchone():
             raise HTTPException(
@@ -271,7 +207,6 @@ async def update_task(request: Request, task_id: str, task: TaskUpdate):
                 detail=f"Task with id '{task_id}' not found"
             )
         
-        # Build update query dynamically based on provided fields
         update_fields = []
         update_values = []
         
@@ -291,75 +226,26 @@ async def update_task(request: Request, task_id: str, task: TaskUpdate):
             update_fields.append("assigned_to = ?")
             update_values.append(task.assigned_to)
         
-        if not update_fields:
-            # No fields to update, just return current task
-            cursor.execute("""
-                SELECT 
-                    t.id,
-                    t.project_id,
-                    t.name,
-                    t.data,
-                    t.status,
-                    t.assigned_to,
-                    t.created_at,
-                    COALESCE(
-                        CAST(COUNT(a.id) AS FLOAT) / NULLIF(
-                            (SELECT COUNT(*) FROM json_each(t.data, '$.items')), 
-                            0
-                        ),
-                        0.0
-                    ) as progress
-                FROM tasks t
-                LEFT JOIN annotations a ON t.id = a.task_id
-                WHERE t.id = ?
-                GROUP BY t.id
-            """, (task_id,))
-            row = cursor.fetchone()
-            data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
-            return TaskResponse(
-                id=row["id"],
-                project_id=row["project_id"],
-                name=row["name"],
-                data=data,
-                status=row["status"],
-                assigned_to=row["assigned_to"],
-                created_at=row["created_at"],
-                progress=row["progress"]
-            )
+        if update_fields:
+            update_values.append(task_id)
+            cursor.execute(f"""
+                UPDATE tasks SET {', '.join(update_fields)} WHERE id = ?
+            """, tuple(update_values))
         
-        # Execute update
-        update_values.append(task_id)
-        cursor.execute(f"""
-            UPDATE tasks
-            SET {', '.join(update_fields)}
-            WHERE id = ?
-        """, update_values)
-        
-        # Fetch and return updated task
         cursor.execute("""
             SELECT 
-                t.id,
-                t.project_id,
-                t.name,
-                t.data,
-                t.status,
-                t.assigned_to,
-                t.created_at,
-                COALESCE(
-                    CAST(COUNT(a.id) AS FLOAT) / NULLIF(
-                        (SELECT COUNT(*) FROM json_each(t.data, '$.items')), 
-                        0
-                    ),
-                    0.0
-                ) as progress
+                t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at,
+                COUNT(a.id) as annotation_count
             FROM tasks t
             LEFT JOIN annotations a ON t.id = a.task_id
             WHERE t.id = ?
-            GROUP BY t.id
+            GROUP BY t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at
         """, (task_id,))
         
         row = cursor.fetchone()
         data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
+        progress = calculate_progress(row["data"], row["annotation_count"])
+        
         return TaskResponse(
             id=row["id"],
             project_id=row["project_id"],
@@ -368,7 +254,7 @@ async def update_task(request: Request, task_id: str, task: TaskUpdate):
             status=row["status"],
             assigned_to=row["assigned_to"],
             created_at=row["created_at"],
-            progress=row["progress"]
+            progress=progress
         )
 
 
@@ -376,18 +262,8 @@ async def update_task(request: Request, task_id: str, task: TaskUpdate):
 async def delete_task(request: Request, task_id: str):
     """
     Delete a task and all associated annotations.
-    
-    Args:
-        request: FastAPI Request object (contains user info)
-        task_id: Task unique identifier
-    
-    Raises:
-        HTTPException: 404 if task not found
-        HTTPException: 403 if user is not admin
-        
     Requires authentication and admin role.
     """
-    # Check if user has admin role
     user = request.state.user
     if user["role"] != "admin":
         raise HTTPException(
@@ -398,7 +274,6 @@ async def delete_task(request: Request, task_id: str):
     with get_db_connection() as conn:
         cursor = conn.cursor()
         
-        # Check if task exists
         cursor.execute("SELECT id FROM tasks WHERE id = ?", (task_id,))
         if not cursor.fetchone():
             raise HTTPException(
@@ -406,9 +281,7 @@ async def delete_task(request: Request, task_id: str):
                 detail=f"Task with id '{task_id}' not found"
             )
         
-        # Delete task (cascade will delete annotations)
         cursor.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
-        
         return None
 
 
@@ -416,23 +289,11 @@ async def delete_task(request: Request, task_id: str):
 async def get_project_tasks(request: Request, project_id: str):
     """
     Get all tasks for a specific project.
-    
-    Args:
-        request: FastAPI Request object (contains user info)
-        project_id: Project unique identifier
-    
-    Returns:
-        List of tasks belonging to the project
-    
-    Raises:
-        HTTPException: 404 if project not found
-        
     Requires authentication.
     """
     with get_db_connection() as conn:
         cursor = conn.cursor()
         
-        # Verify project exists
         cursor.execute("SELECT id FROM projects WHERE id = ?", (project_id,))
         if not cursor.fetchone():
             raise HTTPException(
@@ -440,27 +301,14 @@ async def get_project_tasks(request: Request, project_id: str):
                 detail=f"Project with id '{project_id}' not found"
             )
         
-        # Get all tasks for the project
         cursor.execute("""
             SELECT 
-                t.id,
-                t.project_id,
-                t.name,
-                t.data,
-                t.status,
-                t.assigned_to,
-                t.created_at,
-                COALESCE(
-                    CAST(COUNT(a.id) AS FLOAT) / NULLIF(
-                        (SELECT COUNT(*) FROM json_each(t.data, '$.items')), 
-                        0
-                    ),
-                    0.0
-                ) as progress
+                t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at,
+                COUNT(a.id) as annotation_count
             FROM tasks t
             LEFT JOIN annotations a ON t.id = a.task_id
             WHERE t.project_id = ?
-            GROUP BY t.id
+            GROUP BY t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at
             ORDER BY t.created_at DESC
         """, (project_id,))
         
@@ -468,8 +316,8 @@ async def get_project_tasks(request: Request, project_id: str):
         
         tasks = []
         for row in rows:
-            # Parse JSON data
             data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
+            progress = calculate_progress(row["data"], row["annotation_count"])
             
             tasks.append(TaskResponse(
                 id=row["id"],
@@ -479,7 +327,7 @@ async def get_project_tasks(request: Request, project_id: str):
                 status=row["status"],
                 assigned_to=row["assigned_to"],
                 created_at=row["created_at"],
-                progress=row["progress"]
+                progress=progress
             ))
         
         return tasks

+ 0 - 388
backend/script/init_image_annotation_data.py

@@ -1,388 +0,0 @@
-"""
-初始化图片标注示例数据脚本
-
-创建多种图片标注项目和任务,用于测试图片标注功能。
-包括:目标检测、图像分类、图像分割、关键点标注等。
-"""
-import requests
-import json
-
-# API 基础 URL
-BASE_URL = "http://localhost:8000"
-
-# 1. 目标检测标注配置(矩形框标注)
-OBJECT_DETECTION_CONFIG = """<View>
-  <Header value="目标检测 - 物体识别"/>
-  <Image name="image" value="$image" zoom="true" zoomControl="true" rotateControl="true"/>
-  <RectangleLabels name="label" toName="image">
-    <Label value="人" background="red"/>
-    <Label value="车" background="blue"/>
-    <Label value="自行车" background="green"/>
-    <Label value="狗" background="orange"/>
-    <Label value="猫" background="purple"/>
-  </RectangleLabels>
-</View>"""
-
-# 2. 图像分类标注配置
-IMAGE_CLASSIFICATION_CONFIG = """<View>
-  <Header value="图像分类"/>
-  <Image name="image" value="$image" zoom="true" zoomControl="true" rotateControl="true"/>
-  <Choices name="category" toName="image" choice="single" showInline="true">
-    <Choice value="风景"/>
-    <Choice value="人物"/>
-    <Choice value="动物"/>
-    <Choice value="建筑"/>
-    <Choice value="食物"/>
-    <Choice value="其他"/>
-  </Choices>
-</View>"""
-
-# 3. 图像分割标注配置(多边形标注)
-IMAGE_SEGMENTATION_CONFIG = """<View>
-  <Header value="图像分割 - 精细标注"/>
-  <Image name="image" value="$image" zoom="true" zoomControl="true" rotateControl="true"/>
-  <PolygonLabels name="label" toName="image">
-    <Label value="前景" background="rgba(255, 0, 0, 0.5)"/>
-    <Label value="背景" background="rgba(0, 0, 255, 0.5)"/>
-    <Label value="物体" background="rgba(0, 255, 0, 0.5)"/>
-  </PolygonLabels>
-</View>"""
-
-# 4. 关键点标注配置(人体姿态估计)
-KEYPOINT_DETECTION_CONFIG = """<View>
-  <Header value="关键点标注 - 人体姿态"/>
-  <Image name="image" value="$image" zoom="true" zoomControl="true" rotateControl="true"/>
-  <KeyPointLabels name="keypoint" toName="image">
-    <Label value="头部" background="red"/>
-    <Label value="肩膀" background="blue"/>
-    <Label value="肘部" background="green"/>
-    <Label value="手腕" background="orange"/>
-    <Label value="膝盖" background="purple"/>
-    <Label value="脚踝" background="pink"/>
-  </KeyPointLabels>
-</View>"""
-
-# 5. 多标签图像分类配置
-MULTI_LABEL_CLASSIFICATION_CONFIG = """<View>
-  <Header value="多标签图像分类"/>
-  <Image name="image" value="$image" zoom="true" zoomControl="true" rotateControl="true"/>
-  <Choices name="attributes" toName="image" choice="multiple" showInline="false">
-    <Choice value="室内"/>
-    <Choice value="室外"/>
-    <Choice value="白天"/>
-    <Choice value="夜晚"/>
-    <Choice value="晴天"/>
-    <Choice value="雨天"/>
-    <Choice value="有人"/>
-    <Choice value="无人"/>
-  </Choices>
-</View>"""
-
-# 6. 图像质量评估配置
-IMAGE_QUALITY_CONFIG = """<View>
-  <Header value="图像质量评估"/>
-  <Image name="image" value="$image" zoom="true" zoomControl="true" rotateControl="true"/>
-  <Choices name="quality" toName="image" choice="single" showInline="true">
-    <Choice value="优秀"/>
-    <Choice value="良好"/>
-    <Choice value="一般"/>
-    <Choice value="较差"/>
-  </Choices>
-  <Choices name="issues" toName="image" choice="multiple" showInline="false">
-    <Choice value="模糊"/>
-    <Choice value="曝光过度"/>
-    <Choice value="曝光不足"/>
-    <Choice value="噪点多"/>
-    <Choice value="色彩失真"/>
-  </Choices>
-</View>"""
-
-# 示例图片任务数据
-# 使用公开的测试图片 URL(来自 Unsplash 等免费图片网站)
-SAMPLE_IMAGE_TASKS = [
-    # 目标检测任务
-    {
-        "name": "街道场景-1",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=800"
-        }
-    },
-    {
-        "name": "街道场景-2",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=800"
-        }
-    },
-    {
-        "name": "公园场景",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800"
-        }
-    },
-    
-    # 图像分类任务
-    {
-        "name": "风景图片-1",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
-        }
-    },
-    {
-        "name": "建筑图片-1",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1480714378408-67cf0d13bc1b?w=800"
-        }
-    },
-    {
-        "name": "动物图片-1",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1425082661705-1834bfd09dca?w=800"
-        }
-    },
-    
-    # 图像分割任务
-    {
-        "name": "物体分割-1",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1518791841217-8f162f1e1131?w=800"
-        }
-    },
-    {
-        "name": "物体分割-2",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba?w=800"
-        }
-    },
-    
-    # 关键点标注任务
-    {
-        "name": "人体姿态-1",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1571019614242-c5c5dee9f50b?w=800"
-        }
-    },
-    {
-        "name": "人体姿态-2",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1517836357463-d25dfeac3438?w=800"
-        }
-    },
-    
-    # 多标签分类任务
-    {
-        "name": "场景分析-1",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=800"
-        }
-    },
-    {
-        "name": "场景分析-2",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800"
-        }
-    },
-    
-    # 图像质量评估任务
-    {
-        "name": "质量评估-1",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=800"
-        }
-    },
-    {
-        "name": "质量评估-2",
-        "data": {
-            "image": "https://images.unsplash.com/photo-1501594907352-04cda38ebc29?w=800"
-        }
-    },
-]
-
-
-def create_project(name, description, config):
-    """创建项目"""
-    url = f"{BASE_URL}/api/projects"
-    data = {
-        "name": name,
-        "description": description,
-        "config": config
-    }
-    
-    response = requests.post(url, json=data)
-    if response.status_code == 201:
-        project = response.json()
-        print(f"✓ 创建项目成功: {project['name']} (ID: {project['id']})")
-        return project
-    else:
-        print(f"✗ 创建项目失败: {response.status_code} - {response.text}")
-        return None
-
-
-def create_task(project_id, task_name, task_data):
-    """创建任务"""
-    url = f"{BASE_URL}/api/tasks"
-    data = {
-        "project_id": project_id,
-        "name": task_name,
-        "data": task_data
-    }
-    
-    response = requests.post(url, json=data)
-    if response.status_code == 201:
-        task = response.json()
-        print(f"  ✓ 创建任务: {task['name']} (ID: {task['id']})")
-        return task
-    else:
-        print(f"  ✗ 创建任务失败: {response.status_code} - {response.text}")
-        return None
-
-
-def main():
-    """主函数"""
-    print("=" * 60)
-    print("初始化图片标注平台示例数据")
-    print("=" * 60)
-    print()
-    
-    # 1. 创建目标检测项目
-    print("1. 创建目标检测项目...")
-    detection_project = create_project(
-        name="目标检测 - 街道场景",
-        description="标注街道场景中的人、车、自行车等物体",
-        config=OBJECT_DETECTION_CONFIG
-    )
-    
-    if detection_project:
-        print("   创建目标检测任务...")
-        for i in range(3):
-            create_task(
-                detection_project['id'],
-                SAMPLE_IMAGE_TASKS[i]['name'],
-                SAMPLE_IMAGE_TASKS[i]['data']
-            )
-    print()
-    
-    # 2. 创建图像分类项目
-    print("2. 创建图像分类项目...")
-    classification_project = create_project(
-        name="图像分类",
-        description="将图片分类为风景、人物、动物、建筑、食物等类别",
-        config=IMAGE_CLASSIFICATION_CONFIG
-    )
-    
-    if classification_project:
-        print("   创建图像分类任务...")
-        for i in range(3, 6):
-            create_task(
-                classification_project['id'],
-                SAMPLE_IMAGE_TASKS[i]['name'],
-                SAMPLE_IMAGE_TASKS[i]['data']
-            )
-    print()
-    
-    # 3. 创建图像分割项目
-    print("3. 创建图像分割项目...")
-    segmentation_project = create_project(
-        name="图像分割 - 物体轮廓",
-        description="使用多边形工具精细标注物体轮廓",
-        config=IMAGE_SEGMENTATION_CONFIG
-    )
-    
-    if segmentation_project:
-        print("   创建图像分割任务...")
-        for i in range(6, 8):
-            create_task(
-                segmentation_project['id'],
-                SAMPLE_IMAGE_TASKS[i]['name'],
-                SAMPLE_IMAGE_TASKS[i]['data']
-            )
-    print()
-    
-    # 4. 创建关键点标注项目
-    print("4. 创建关键点标注项目...")
-    keypoint_project = create_project(
-        name="关键点标注 - 人体姿态估计",
-        description="标注人体关键点(头部、肩膀、肘部、手腕、膝盖、脚踝)",
-        config=KEYPOINT_DETECTION_CONFIG
-    )
-    
-    if keypoint_project:
-        print("   创建关键点标注任务...")
-        for i in range(8, 10):
-            create_task(
-                keypoint_project['id'],
-                SAMPLE_IMAGE_TASKS[i]['name'],
-                SAMPLE_IMAGE_TASKS[i]['data']
-            )
-    print()
-    
-    # 5. 创建多标签分类项目
-    print("5. 创建多标签分类项目...")
-    multi_label_project = create_project(
-        name="多标签图像分类",
-        description="为图片添加多个属性标签(室内/室外、白天/夜晚等)",
-        config=MULTI_LABEL_CLASSIFICATION_CONFIG
-    )
-    
-    if multi_label_project:
-        print("   创建多标签分类任务...")
-        for i in range(10, 12):
-            create_task(
-                multi_label_project['id'],
-                SAMPLE_IMAGE_TASKS[i]['name'],
-                SAMPLE_IMAGE_TASKS[i]['data']
-            )
-    print()
-    
-    # 6. 创建图像质量评估项目
-    print("6. 创建图像质量评估项目...")
-    quality_project = create_project(
-        name="图像质量评估",
-        description="评估图片质量并标注存在的问题(模糊、曝光等)",
-        config=IMAGE_QUALITY_CONFIG
-    )
-    
-    if quality_project:
-        print("   创建图像质量评估任务...")
-        for i in range(12, 14):
-            create_task(
-                quality_project['id'],
-                SAMPLE_IMAGE_TASKS[i]['name'],
-                SAMPLE_IMAGE_TASKS[i]['data']
-            )
-    print()
-    
-    print("=" * 60)
-    print("图片标注示例数据初始化完成!")
-    print("=" * 60)
-    print()
-    print("已创建的项目类型:")
-    print("1. 目标检测 - 使用矩形框标注物体")
-    print("2. 图像分类 - 单标签分类")
-    print("3. 图像分割 - 使用多边形精细标注")
-    print("4. 关键点标注 - 人体姿态估计")
-    print("5. 多标签分类 - 场景属性标注")
-    print("6. 图像质量评估 - 质量评分和问题标注")
-    print()
-    print("你现在可以:")
-    print("1. 访问 http://localhost:4200/projects 查看项目列表")
-    print("2. 点击项目查看详情和任务")
-    print("3. 点击'开始标注'按钮进行图片标注")
-    print()
-    print("注意:")
-    print("- 图片来自 Unsplash 免费图片库")
-    print("- 需要网络连接才能加载图片")
-    print("- 如果图片加载失败,请检查网络连接")
-    print()
-
-
-if __name__ == "__main__":
-    try:
-        main()
-    except requests.exceptions.ConnectionError:
-        print("✗ 错误: 无法连接到后端服务器")
-        print("  请确保后端服务器正在运行:")
-        print("  cd backend && python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000")
-    except Exception as e:
-        print(f"✗ 发生错误: {e}")
-        import traceback
-        traceback.print_exc()

+ 0 - 203
backend/script/init_sample_data.py

@@ -1,203 +0,0 @@
-"""
-初始化示例数据脚本
-
-创建一个标准的文本标注项目和任务,用于测试标注功能。
-"""
-import requests
-import json
-
-# API 基础 URL
-BASE_URL = "http://localhost:8000"
-
-# 文本分类标注配置(LabelStudio XML 格式)
-TEXT_CLASSIFICATION_CONFIG = """<View>
-  <Header value="文本分类标注"/>
-  <Text name="text" value="$text"/>
-  <Choices name="sentiment" toName="text" choice="single" showInline="true">
-    <Choice value="正面"/>
-    <Choice value="负面"/>
-    <Choice value="中性"/>
-  </Choices>
-</View>"""
-
-# 命名实体识别标注配置
-NER_CONFIG = """<View>
-  <Header value="命名实体识别"/>
-  <Text name="text" value="$text"/>
-  <Labels name="label" toName="text">
-    <Label value="人名" background="red"/>
-    <Label value="地名" background="blue"/>
-    <Label value="机构名" background="green"/>
-    <Label value="时间" background="orange"/>
-  </Labels>
-</View>"""
-
-# 文本标注配置(高亮标注)
-TEXT_HIGHLIGHT_CONFIG = """<View>
-  <Header value="文本高亮标注"/>
-  <Text name="text" value="$text"/>
-  <Labels name="label" toName="text">
-    <Label value="重要信息" background="yellow"/>
-    <Label value="关键词" background="lightblue"/>
-    <Label value="问题" background="pink"/>
-  </Labels>
-</View>"""
-
-# 示例任务数据
-SAMPLE_TASKS = [
-    {
-        "name": "文本分类任务-1",
-        "data": {
-            "text": "这家餐厅的服务态度非常好,菜品也很美味,环境优雅,强烈推荐!"
-        }
-    },
-    {
-        "name": "文本分类任务-2",
-        "data": {
-            "text": "产品质量太差了,用了不到一周就坏了,客服态度也很恶劣,非常失望。"
-        }
-    },
-    {
-        "name": "文本分类任务-3",
-        "data": {
-            "text": "这款手机性能一般,价格适中,适合日常使用。"
-        }
-    },
-    {
-        "name": "命名实体识别任务-1",
-        "data": {
-            "text": "2024年1月15日,张三在北京大学参加了人工智能研讨会。"
-        }
-    },
-    {
-        "name": "命名实体识别任务-2",
-        "data": {
-            "text": "李明是清华大学的教授,他在上海交通大学获得了博士学位。"
-        }
-    },
-    {
-        "name": "文本高亮任务-1",
-        "data": {
-            "text": "机器学习是人工智能的一个重要分支,它使计算机能够从数据中学习并做出决策。深度学习是机器学习的一个子领域,使用神经网络来模拟人脑的工作方式。"
-        }
-    }
-]
-
-
-def create_project(name, description, config):
-    """创建项目"""
-    url = f"{BASE_URL}/api/projects"
-    data = {
-        "name": name,
-        "description": description,
-        "config": config
-    }
-    
-    response = requests.post(url, json=data)
-    if response.status_code == 201:
-        project = response.json()
-        print(f"✓ 创建项目成功: {project['name']} (ID: {project['id']})")
-        return project
-    else:
-        print(f"✗ 创建项目失败: {response.status_code} - {response.text}")
-        return None
-
-
-def create_task(project_id, task_name, task_data):
-    """创建任务"""
-    url = f"{BASE_URL}/api/tasks"
-    data = {
-        "project_id": project_id,
-        "name": task_name,
-        "data": task_data
-    }
-    
-    response = requests.post(url, json=data)
-    if response.status_code == 201:
-        task = response.json()
-        print(f"  ✓ 创建任务: {task['name']} (ID: {task['id']})")
-        return task
-    else:
-        print(f"  ✗ 创建任务失败: {response.status_code} - {response.text}")
-        return None
-
-
-def main():
-    """主函数"""
-    print("=" * 60)
-    print("初始化标注平台示例数据")
-    print("=" * 60)
-    print()
-    
-    # 1. 创建文本分类项目
-    print("1. 创建文本分类项目...")
-    classification_project = create_project(
-        name="情感分析标注项目",
-        description="对用户评论进行情感分类(正面/负面/中性)",
-        config=TEXT_CLASSIFICATION_CONFIG
-    )
-    
-    if classification_project:
-        print("   创建文本分类任务...")
-        for i in range(3):
-            create_task(
-                classification_project['id'],
-                SAMPLE_TASKS[i]['name'],
-                SAMPLE_TASKS[i]['data']
-            )
-    print()
-    
-    # 2. 创建命名实体识别项目
-    print("2. 创建命名实体识别项目...")
-    ner_project = create_project(
-        name="命名实体识别项目",
-        description="识别文本中的人名、地名、机构名和时间等实体",
-        config=NER_CONFIG
-    )
-    
-    if ner_project:
-        print("   创建命名实体识别任务...")
-        for i in range(3, 5):
-            create_task(
-                ner_project['id'],
-                SAMPLE_TASKS[i]['name'],
-                SAMPLE_TASKS[i]['data']
-            )
-    print()
-    
-    # 3. 创建文本高亮项目
-    print("3. 创建文本高亮标注项目...")
-    highlight_project = create_project(
-        name="文本高亮标注项目",
-        description="标记文本中的重要信息、关键词和问题",
-        config=TEXT_HIGHLIGHT_CONFIG
-    )
-    
-    if highlight_project:
-        print("   创建文本高亮任务...")
-        create_task(
-            highlight_project['id'],
-            SAMPLE_TASKS[5]['name'],
-            SAMPLE_TASKS[5]['data']
-        )
-    print()
-    
-    print("=" * 60)
-    print("示例数据初始化完成!")
-    print("=" * 60)
-    print()
-    print("你现在可以:")
-    print("1. 访问 http://localhost:4200/projects 查看项目列表")
-    print("2. 点击项目查看详情和任务")
-    print("3. 点击'开始标注'按钮进行标注")
-    print()
-
-
-if __name__ == "__main__":
-    try:
-        main()
-    except requests.exceptions.ConnectionError:
-        print("✗ 错误: 无法连接到后端服务器")
-        print("  请确保后端服务器正在运行: python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000")
-    except Exception as e:
-        print(f"✗ 发生错误: {e}")

+ 0 - 172
backend/script/test_annotation.py

@@ -1,172 +0,0 @@
-"""
-测试标注功能脚本
-
-快速测试标注平台的完整流程。
-"""
-import requests
-import json
-
-BASE_URL = "http://localhost:8000"
-
-
-def test_complete_workflow():
-    """测试完整的标注工作流程"""
-    print("=" * 60)
-    print("测试标注平台完整工作流程")
-    print("=" * 60)
-    print()
-    
-    # 1. 创建项目
-    print("1. 创建测试项目...")
-    project_data = {
-        "name": "测试项目",
-        "description": "用于测试的简单文本分类项目",
-        "config": """<View>
-  <Text name="text" value="$text"/>
-  <Choices name="label" toName="text" choice="single">
-    <Choice value="类别A"/>
-    <Choice value="类别B"/>
-  </Choices>
-</View>"""
-    }
-    
-    response = requests.post(f"{BASE_URL}/api/projects", json=project_data)
-    if response.status_code != 201:
-        print(f"✗ 创建项目失败: {response.text}")
-        return
-    
-    project = response.json()
-    print(f"✓ 项目创建成功: {project['id']}")
-    print()
-    
-    # 2. 创建任务
-    print("2. 创建测试任务...")
-    task_data = {
-        "project_id": project['id'],
-        "name": "测试任务",
-        "data": {
-            "text": "这是一个测试文本"
-        }
-    }
-    
-    response = requests.post(f"{BASE_URL}/api/tasks", json=task_data)
-    if response.status_code != 201:
-        print(f"✗ 创建任务失败: {response.text}")
-        return
-    
-    task = response.json()
-    print(f"✓ 任务创建成功: {task['id']}")
-    print()
-    
-    # 3. 模拟标注
-    print("3. 创建标注...")
-    annotation_data = {
-        "task_id": task['id'],
-        "user_id": "test_user",
-        "result": {
-            "label": "类别A"
-        }
-    }
-    
-    response = requests.post(f"{BASE_URL}/api/annotations", json=annotation_data)
-    if response.status_code != 201:
-        print(f"✗ 创建标注失败: {response.text}")
-        return
-    
-    annotation = response.json()
-    print(f"✓ 标注创建成功: {annotation['id']}")
-    print()
-    
-    # 4. 查询标注
-    print("4. 查询标注结果...")
-    response = requests.get(f"{BASE_URL}/api/annotations/{annotation['id']}")
-    if response.status_code != 200:
-        print(f"✗ 查询标注失败: {response.text}")
-        return
-    
-    result = response.json()
-    print(f"✓ 标注结果: {json.dumps(result, indent=2, ensure_ascii=False)}")
-    print()
-    
-    # 5. 更新任务状态
-    print("5. 更新任务状态...")
-    response = requests.put(
-        f"{BASE_URL}/api/tasks/{task['id']}",
-        json={"status": "completed"}
-    )
-    if response.status_code != 200:
-        print(f"✗ 更新任务失败: {response.text}")
-        return
-    
-    updated_task = response.json()
-    print(f"✓ 任务状态更新为: {updated_task['status']}")
-    print()
-    
-    # 6. 清理测试数据
-    print("6. 清理测试数据...")
-    requests.delete(f"{BASE_URL}/api/projects/{project['id']}")
-    print("✓ 测试数据已清理")
-    print()
-    
-    print("=" * 60)
-    print("✓ 所有测试通过!")
-    print("=" * 60)
-
-
-def check_sample_data():
-    """检查示例数据"""
-    print("=" * 60)
-    print("检查示例数据")
-    print("=" * 60)
-    print()
-    
-    # 查询项目
-    print("项目列表:")
-    response = requests.get(f"{BASE_URL}/api/projects")
-    if response.status_code == 200:
-        projects = response.json()
-        for project in projects:
-            print(f"  - {project['name']} (ID: {project['id']}, 任务数: {project['task_count']})")
-    else:
-        print(f"  ✗ 查询失败: {response.text}")
-    print()
-    
-    # 查询任务
-    print("任务列表:")
-    response = requests.get(f"{BASE_URL}/api/tasks")
-    if response.status_code == 200:
-        tasks = response.json()
-        for task in tasks:
-            print(f"  - {task['name']} (状态: {task['status']}, 进度: {task['progress']}%)")
-    else:
-        print(f"  ✗ 查询失败: {response.text}")
-    print()
-    
-    # 查询标注
-    print("标注列表:")
-    response = requests.get(f"{BASE_URL}/api/annotations")
-    if response.status_code == 200:
-        annotations = response.json()
-        print(f"  共 {len(annotations)} 条标注记录")
-        for annotation in annotations[:5]:  # 只显示前5条
-            print(f"  - 任务: {annotation['task_id']}, 用户: {annotation['user_id']}")
-    else:
-        print(f"  ✗ 查询失败: {response.text}")
-    print()
-
-
-if __name__ == "__main__":
-    import sys
-    
-    try:
-        if len(sys.argv) > 1 and sys.argv[1] == "check":
-            check_sample_data()
-        else:
-            test_complete_workflow()
-    except requests.exceptions.ConnectionError:
-        print("✗ 错误: 无法连接到后端服务器")
-        print("  请确保后端服务器正在运行: python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000")
-    except Exception as e:
-        print(f"✗ 发生错误: {e}")
-        import traceback
-        traceback.print_exc()

+ 0 - 166
backend/script/test_auth_flow.ps1

@@ -1,166 +0,0 @@
-# JWT Authentication System Test Script
-# Tests the complete authentication flow
-
-Write-Host "`n=== JWT Authentication System Test ===" -ForegroundColor Cyan
-
-# Test 1: Register a new user
-Write-Host "`n[Test 1] User Registration..." -ForegroundColor Yellow
-$registerBody = @{
-    username = "testuser2"
-    email = "test2@example.com"
-    password = "password123"
-} | ConvertTo-Json
-
-try {
-    $registerResponse = Invoke-RestMethod -Uri "http://localhost:8000/api/auth/register" `
-        -Method Post -ContentType "application/json" -Body $registerBody
-    Write-Host "✓ Registration successful!" -ForegroundColor Green
-    Write-Host "  User ID: $($registerResponse.user.id)"
-    Write-Host "  Username: $($registerResponse.user.username)"
-    Write-Host "  Role: $($registerResponse.user.role)"
-} catch {
-    Write-Host "✗ Registration failed: $($_.Exception.Message)" -ForegroundColor Red
-}
-
-# Test 2: Login with credentials
-Write-Host "`n[Test 2] User Login..." -ForegroundColor Yellow
-$loginBody = @{
-    username = "testuser2"
-    password = "password123"
-} | ConvertTo-Json
-
-try {
-    $loginResponse = Invoke-RestMethod -Uri "http://localhost:8000/api/auth/login" `
-        -Method Post -ContentType "application/json" -Body $loginBody
-    Write-Host "✓ Login successful!" -ForegroundColor Green
-    Write-Host "  Access Token: $($loginResponse.access_token.Substring(0,50))..."
-    $token = $loginResponse.access_token
-    $refreshToken = $loginResponse.refresh_token
-} catch {
-    Write-Host "✗ Login failed: $($_.Exception.Message)" -ForegroundColor Red
-    exit 1
-}
-
-# Test 3: Access protected endpoint with token
-Write-Host "`n[Test 3] Access Protected Endpoint (with token)..." -ForegroundColor Yellow
-$headers = @{
-    Authorization = "Bearer $token"
-}
-
-try {
-    $meResponse = Invoke-RestMethod -Uri "http://localhost:8000/api/auth/me" `
-        -Method Get -Headers $headers
-    Write-Host "✓ Successfully accessed protected endpoint!" -ForegroundColor Green
-    Write-Host "  User: $($meResponse.username)"
-    Write-Host "  Email: $($meResponse.email)"
-} catch {
-    Write-Host "✗ Failed to access protected endpoint: $($_.Exception.Message)" -ForegroundColor Red
-}
-
-# Test 4: Access protected endpoint without token
-Write-Host "`n[Test 4] Access Protected Endpoint (without token)..." -ForegroundColor Yellow
-try {
-    $response = Invoke-WebRequest -Uri "http://localhost:8000/api/projects" `
-        -Method Get -ErrorAction Stop
-    Write-Host "✗ Should have been rejected!" -ForegroundColor Red
-} catch {
-    if ($_.Exception.Response.StatusCode.value__ -eq 401) {
-        Write-Host "✓ Correctly rejected with 401 Unauthorized!" -ForegroundColor Green
-    } else {
-        Write-Host "✗ Wrong status code: $($_.Exception.Response.StatusCode.value__)" -ForegroundColor Red
-    }
-}
-
-# Test 5: Create a project (authenticated)
-Write-Host "`n[Test 5] Create Project (authenticated)..." -ForegroundColor Yellow
-$projectBody = @{
-    name = "Test Project"
-    description = "A test project"
-    config = '{"type":"image"}'
-} | ConvertTo-Json
-
-try {
-    $projectResponse = Invoke-RestMethod -Uri "http://localhost:8000/api/projects" `
-        -Method Post -Headers $headers -ContentType "application/json" -Body $projectBody
-    Write-Host "✓ Project created successfully!" -ForegroundColor Green
-    Write-Host "  Project ID: $($projectResponse.id)"
-    Write-Host "  Project Name: $($projectResponse.name)"
-    $projectId = $projectResponse.id
-} catch {
-    Write-Host "✗ Failed to create project: $($_.Exception.Message)" -ForegroundColor Red
-}
-
-# Test 6: Create a task (authenticated, auto-assigned to current user)
-Write-Host "`n[Test 6] Create Task (auto-assigned to current user)..." -ForegroundColor Yellow
-$taskBody = @{
-    project_id = $projectId
-    name = "Test Task"
-    data = @{
-        items = @(
-            @{ id = 1; text = "Sample text" }
-        )
-    }
-} | ConvertTo-Json -Depth 5
-
-try {
-    $taskResponse = Invoke-RestMethod -Uri "http://localhost:8000/api/tasks" `
-        -Method Post -Headers $headers -ContentType "application/json" -Body $taskBody
-    Write-Host "✓ Task created successfully!" -ForegroundColor Green
-    Write-Host "  Task ID: $($taskResponse.id)"
-    Write-Host "  Assigned to: $($taskResponse.assigned_to)"
-    $taskId = $taskResponse.id
-} catch {
-    Write-Host "✗ Failed to create task: $($_.Exception.Message)" -ForegroundColor Red
-}
-
-# Test 7: Create an annotation (authenticated, auto-assigned to current user)
-Write-Host "`n[Test 7] Create Annotation (auto-assigned to current user)..." -ForegroundColor Yellow
-$annotationBody = @{
-    task_id = $taskId
-    user_id = "ignored"  # This should be ignored and use authenticated user
-    result = @{
-        label = "positive"
-        confidence = 0.95
-    }
-} | ConvertTo-Json -Depth 5
-
-try {
-    $annotationResponse = Invoke-RestMethod -Uri "http://localhost:8000/api/annotations" `
-        -Method Post -Headers $headers -ContentType "application/json" -Body $annotationBody
-    Write-Host "✓ Annotation created successfully!" -ForegroundColor Green
-    Write-Host "  Annotation ID: $($annotationResponse.id)"
-    Write-Host "  User ID: $($annotationResponse.user_id)"
-} catch {
-    Write-Host "✗ Failed to create annotation: $($_.Exception.Message)" -ForegroundColor Red
-}
-
-# Test 8: Token refresh
-Write-Host "`n[Test 8] Token Refresh..." -ForegroundColor Yellow
-$refreshBody = @{
-    refresh_token = $refreshToken
-} | ConvertTo-Json
-
-try {
-    $refreshResponse = Invoke-RestMethod -Uri "http://localhost:8000/api/auth/refresh" `
-        -Method Post -ContentType "application/json" -Body $refreshBody
-    Write-Host "✓ Token refreshed successfully!" -ForegroundColor Green
-    Write-Host "  New Access Token: $($refreshResponse.access_token.Substring(0,50))..."
-} catch {
-    Write-Host "✗ Failed to refresh token: $($_.Exception.Message)" -ForegroundColor Red
-}
-
-# Test 9: Try to delete project as non-admin (should fail)
-Write-Host "`n[Test 9] Delete Project (as non-admin, should fail)..." -ForegroundColor Yellow
-try {
-    $response = Invoke-WebRequest -Uri "http://localhost:8000/api/projects/$projectId" `
-        -Method Delete -Headers $headers -ErrorAction Stop
-    Write-Host "✗ Should have been rejected!" -ForegroundColor Red
-} catch {
-    if ($_.Exception.Response.StatusCode.value__ -eq 403) {
-        Write-Host "✓ Correctly rejected with 403 Forbidden!" -ForegroundColor Green
-    } else {
-        Write-Host "✗ Wrong status code: $($_.Exception.Response.StatusCode.value__)" -ForegroundColor Red
-    }
-}
-
-Write-Host "`n=== All Tests Completed ===" -ForegroundColor Cyan

+ 131 - 0
backend/scripts/migrate_sqlite_to_mysql.py

@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+"""
+SQLite 到 MySQL 数据迁移脚本
+
+用法:
+    python scripts/migrate_sqlite_to_mysql.py [sqlite_db_path]
+
+示例:
+    python scripts/migrate_sqlite_to_mysql.py annotation_platform.db
+"""
+import sys
+import os
+import sqlite3
+
+# 添加 backend 目录到路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+import pymysql
+from config import settings
+
+
+def get_sqlite_connection(db_path: str):
+    """获取 SQLite 连接"""
+    conn = sqlite3.connect(db_path)
+    conn.row_factory = sqlite3.Row
+    return conn
+
+
+def get_mysql_connection():
+    """获取 MySQL 连接"""
+    return pymysql.connect(
+        host=settings.MYSQL_HOST,
+        port=settings.MYSQL_PORT,
+        user=settings.MYSQL_USER,
+        password=settings.MYSQL_PASSWORD,
+        database=settings.MYSQL_DATABASE,
+        charset='utf8mb4',
+        cursorclass=pymysql.cursors.DictCursor
+    )
+
+
+def migrate_table(sqlite_conn, mysql_conn, table_name: str, columns: list):
+    """迁移单个表的数据"""
+    sqlite_cursor = sqlite_conn.cursor()
+    mysql_cursor = mysql_conn.cursor()
+    
+    # 查询 SQLite 数据
+    sqlite_cursor.execute(f"SELECT * FROM {table_name}")
+    rows = sqlite_cursor.fetchall()
+    
+    if not rows:
+        print(f"  {table_name}: 无数据")
+        return 0
+    
+    # 构建 INSERT 语句
+    placeholders = ', '.join(['%s'] * len(columns))
+    columns_str = ', '.join(columns)
+    insert_sql = f"INSERT INTO {table_name} ({columns_str}) VALUES ({placeholders})"
+    
+    # 批量插入
+    count = 0
+    for row in rows:
+        try:
+            values = tuple(row[col] for col in columns)
+            mysql_cursor.execute(insert_sql, values)
+            count += 1
+        except pymysql.err.IntegrityError as e:
+            if e.args[0] == 1062:  # Duplicate entry
+                print(f"  跳过重复记录: {row['id']}")
+            else:
+                raise
+    
+    mysql_conn.commit()
+    print(f"  {table_name}: 迁移 {count} 条记录")
+    return count
+
+
+def main():
+    # 获取 SQLite 数据库路径
+    if len(sys.argv) > 1:
+        sqlite_path = sys.argv[1]
+    else:
+        sqlite_path = os.path.join(os.path.dirname(__file__), '..', 'annotation_platform.db')
+    
+    if not os.path.exists(sqlite_path):
+        print(f"错误: SQLite 数据库不存在: {sqlite_path}")
+        sys.exit(1)
+    
+    print(f"源数据库: {sqlite_path}")
+    print(f"目标数据库: {settings.MYSQL_HOST}:{settings.MYSQL_PORT}/{settings.MYSQL_DATABASE}")
+    print()
+    
+    # 连接数据库
+    sqlite_conn = get_sqlite_connection(sqlite_path)
+    mysql_conn = get_mysql_connection()
+    
+    try:
+        # 表结构和列定义
+        tables = {
+            'users': ['id', 'username', 'email', 'password_hash', 'role', 
+                      'oauth_provider', 'oauth_id', 'created_at', 'updated_at'],
+            'projects': ['id', 'name', 'description', 'config', 'created_at'],
+            'tasks': ['id', 'project_id', 'name', 'data', 'status', 
+                      'assigned_to', 'created_at'],
+            'annotations': ['id', 'task_id', 'user_id', 'result', 
+                           'created_at', 'updated_at'],
+        }
+        
+        print("开始迁移...")
+        total = 0
+        
+        # 按顺序迁移(考虑外键约束)
+        for table_name in ['users', 'projects', 'tasks', 'annotations']:
+            columns = tables[table_name]
+            count = migrate_table(sqlite_conn, mysql_conn, table_name, columns)
+            total += count
+        
+        print()
+        print(f"迁移完成!共迁移 {total} 条记录")
+        
+    except Exception as e:
+        print(f"迁移失败: {e}")
+        mysql_conn.rollback()
+        sys.exit(1)
+    finally:
+        sqlite_conn.close()
+        mysql_conn.close()
+
+
+if __name__ == '__main__':
+    main()

+ 6 - 0
web/learn.md

@@ -1,3 +1,9 @@
+
+yarn nx serve lq_label    
+
+
+
+
 # 2. 启动开发服务器(带 HMR)
 
 yarn ls:dev