|
|
@@ -0,0 +1,885 @@
|
|
|
+# Design Document: 任务分配与模板管理
|
|
|
+
|
|
|
+## Overview
|
|
|
+
|
|
|
+本设计文档描述了标注平台的任务分配与模板管理功能的技术实现方案。该功能将完善现有平台的工作流程,实现从管理员(数据源)→ 任务派发 → 人员标注任务分配 → 数据收集 → 数据导出的完整闭环。
|
|
|
+
|
|
|
+主要包含两大模块:
|
|
|
+- **任务分配系统**:支持管理员将任务分配给特定标注人员,标注人员只能看到分配给自己的任务
|
|
|
+- **模板管理系统**:提供类似 LabelStudio 的模板选择界面和自定义 XML 配置编辑器
|
|
|
+
|
|
|
+技术栈:
|
|
|
+- **前端**: React + TypeScript + Jotai + @humansignal/ui + Monaco Editor
|
|
|
+- **后端**: Python FastAPI + SQLite/MySQL
|
|
|
+- **编辑器**: Monaco Editor (XML 语法高亮) + @humansignal/editor (预览)
|
|
|
+
|
|
|
+## Architecture
|
|
|
+
|
|
|
+### System Architecture
|
|
|
+
|
|
|
+```mermaid
|
|
|
+graph TB
|
|
|
+ subgraph "Frontend (web/apps/lq_label)"
|
|
|
+ A[React App] --> B[Layout Component]
|
|
|
+ B --> C[User Management View]
|
|
|
+ B --> D[Task Assignment View]
|
|
|
+ B --> E[Template Manager View]
|
|
|
+ B --> F[Config Editor View]
|
|
|
+ B --> G[Export View]
|
|
|
+
|
|
|
+ E --> H[Template Gallery]
|
|
|
+ E --> I[Template Preview]
|
|
|
+
|
|
|
+ F --> J[Monaco Editor]
|
|
|
+ F --> K[Visual Builder]
|
|
|
+ F --> L[Live Preview]
|
|
|
+
|
|
|
+ A --> M[Jotai State Management]
|
|
|
+ end
|
|
|
+
|
|
|
+ subgraph "Backend (backend/)"
|
|
|
+ N[FastAPI Server] --> O[User API]
|
|
|
+ N --> P[Task Assignment API]
|
|
|
+ N --> Q[Template API]
|
|
|
+ N --> R[Export API]
|
|
|
+ N --> S[Statistics API]
|
|
|
+
|
|
|
+ O --> T[Database]
|
|
|
+ P --> T
|
|
|
+ Q --> T
|
|
|
+ R --> T
|
|
|
+ S --> T
|
|
|
+ end
|
|
|
+
|
|
|
+ A -->|HTTP/REST| N
|
|
|
+```
|
|
|
+
|
|
|
+### Task Assignment Flow
|
|
|
+
|
|
|
+```mermaid
|
|
|
+sequenceDiagram
|
|
|
+ participant Admin as 管理员
|
|
|
+ participant Frontend as 前端
|
|
|
+ participant Backend as 后端
|
|
|
+ participant DB as 数据库
|
|
|
+
|
|
|
+ Admin->>Frontend: 选择任务
|
|
|
+ Admin->>Frontend: 点击分配按钮
|
|
|
+ Frontend->>Backend: GET /api/users?role=annotator
|
|
|
+ Backend->>DB: 查询标注人员列表
|
|
|
+ DB-->>Backend: 返回用户列表
|
|
|
+ Backend-->>Frontend: 返回用户列表
|
|
|
+ Frontend->>Admin: 显示用户选择对话框
|
|
|
+ Admin->>Frontend: 选择用户并确认
|
|
|
+ Frontend->>Backend: PUT /api/tasks/{id}/assign
|
|
|
+ Backend->>DB: 更新任务分配
|
|
|
+ DB-->>Backend: 确认更新
|
|
|
+ Backend-->>Frontend: 返回更新后的任务
|
|
|
+ Frontend->>Admin: 显示分配成功
|
|
|
+```
|
|
|
+
|
|
|
+### Template Selection Flow
|
|
|
+
|
|
|
+```mermaid
|
|
|
+sequenceDiagram
|
|
|
+ participant Admin as 管理员
|
|
|
+ participant Frontend as 前端
|
|
|
+ participant Editor as 编辑器
|
|
|
+
|
|
|
+ Admin->>Frontend: 创建项目
|
|
|
+ Frontend->>Admin: 显示模板选择界面
|
|
|
+ Admin->>Frontend: 选择模板类别
|
|
|
+ Frontend->>Admin: 显示模板列表
|
|
|
+ Admin->>Frontend: 选择具体模板
|
|
|
+ Frontend->>Editor: 加载模板配置
|
|
|
+ Editor->>Admin: 显示预览效果
|
|
|
+ Admin->>Frontend: 确认使用模板
|
|
|
+ Frontend->>Frontend: 填充配置到项目表单
|
|
|
+```
|
|
|
+
|
|
|
+## Components and Interfaces
|
|
|
+
|
|
|
+### Frontend Components
|
|
|
+
|
|
|
+#### 1. UserManagementView
|
|
|
+**Location**: `web/apps/lq_label/src/views/user-management-view/`
|
|
|
+
|
|
|
+**Responsibility**: 用户管理界面,显示用户列表和任务统计
|
|
|
+
|
|
|
+**Interface**:
|
|
|
+```typescript
|
|
|
+interface UserManagementViewProps {}
|
|
|
+
|
|
|
+interface UserWithStats {
|
|
|
+ id: string;
|
|
|
+ username: string;
|
|
|
+ email: string;
|
|
|
+ role: 'admin' | 'annotator' | 'viewer';
|
|
|
+ created_at: string;
|
|
|
+ task_stats: {
|
|
|
+ assigned_count: number;
|
|
|
+ completed_count: number;
|
|
|
+ annotation_count: number;
|
|
|
+ completion_rate: number;
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Features**:
|
|
|
+- 用户列表展示(使用 DataTable)
|
|
|
+- 角色筛选
|
|
|
+- 用户搜索
|
|
|
+- 任务统计显示
|
|
|
+
|
|
|
+#### 2. TaskAssignmentDialog
|
|
|
+**Location**: `web/apps/lq_label/src/components/task-assignment-dialog/`
|
|
|
+
|
|
|
+**Responsibility**: 任务分配对话框
|
|
|
+
|
|
|
+**Interface**:
|
|
|
+```typescript
|
|
|
+interface TaskAssignmentDialogProps {
|
|
|
+ open: boolean;
|
|
|
+ taskIds: string[];
|
|
|
+ onClose: () => void;
|
|
|
+ onAssign: (userId: string) => Promise<void>;
|
|
|
+}
|
|
|
+
|
|
|
+interface AssignableUser {
|
|
|
+ id: string;
|
|
|
+ username: string;
|
|
|
+ email: string;
|
|
|
+ current_task_count: number;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Features**:
|
|
|
+- 用户选择列表
|
|
|
+- 当前工作量显示
|
|
|
+- 批量分配支持
|
|
|
+- 平均分配选项
|
|
|
+
|
|
|
+#### 3. TemplateGallery
|
|
|
+**Location**: `web/apps/lq_label/src/components/template-gallery/`
|
|
|
+
|
|
|
+**Responsibility**: 模板选择画廊
|
|
|
+
|
|
|
+**Interface**:
|
|
|
+```typescript
|
|
|
+interface TemplateGalleryProps {
|
|
|
+ onSelect: (template: Template) => void;
|
|
|
+ selectedId?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface Template {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ category: TemplateCategory;
|
|
|
+ description: string;
|
|
|
+ config: string;
|
|
|
+ preview_image?: string;
|
|
|
+ tags: string[];
|
|
|
+}
|
|
|
+
|
|
|
+type TemplateCategory =
|
|
|
+ | 'image_classification'
|
|
|
+ | 'object_detection'
|
|
|
+ | 'image_segmentation'
|
|
|
+ | 'text_classification'
|
|
|
+ | 'ner'
|
|
|
+ | 'text_labeling'
|
|
|
+ | 'audio_transcription'
|
|
|
+ | 'video_annotation';
|
|
|
+```
|
|
|
+
|
|
|
+**Features**:
|
|
|
+- 分类筛选
|
|
|
+- 模板搜索
|
|
|
+- 模板预览卡片
|
|
|
+- 选中状态显示
|
|
|
+
|
|
|
+#### 4. ConfigEditor
|
|
|
+**Location**: `web/apps/lq_label/src/components/config-editor/`
|
|
|
+
|
|
|
+**Responsibility**: 标注配置编辑器(代码模式 + 可视化模式)
|
|
|
+
|
|
|
+**Interface**:
|
|
|
+```typescript
|
|
|
+interface ConfigEditorProps {
|
|
|
+ value: string;
|
|
|
+ onChange: (value: string) => void;
|
|
|
+ mode: 'code' | 'visual';
|
|
|
+ onModeChange: (mode: 'code' | 'visual') => void;
|
|
|
+ previewData?: Record<string, any>;
|
|
|
+}
|
|
|
+
|
|
|
+interface ValidationResult {
|
|
|
+ valid: boolean;
|
|
|
+ errors: Array<{
|
|
|
+ line: number;
|
|
|
+ column: number;
|
|
|
+ message: string;
|
|
|
+ }>;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Features**:
|
|
|
+- Monaco Editor 集成(XML 语法高亮)
|
|
|
+- 实时语法验证
|
|
|
+- 错误提示
|
|
|
+- 模式切换
|
|
|
+
|
|
|
+#### 5. VisualConfigBuilder
|
|
|
+**Location**: `web/apps/lq_label/src/components/visual-config-builder/`
|
|
|
+
|
|
|
+**Responsibility**: 可视化配置构建器
|
|
|
+
|
|
|
+**Interface**:
|
|
|
+```typescript
|
|
|
+interface VisualConfigBuilderProps {
|
|
|
+ value: string;
|
|
|
+ onChange: (value: string) => void;
|
|
|
+}
|
|
|
+
|
|
|
+interface ConfigComponent {
|
|
|
+ type: string;
|
|
|
+ name: string;
|
|
|
+ icon: React.ReactNode;
|
|
|
+ category: 'data' | 'annotation' | 'layout';
|
|
|
+ defaultProps: Record<string, any>;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Features**:
|
|
|
+- 组件面板
|
|
|
+- 拖拽构建
|
|
|
+- 属性编辑面板
|
|
|
+- 组件树视图
|
|
|
+
|
|
|
+#### 6. ConfigPreview
|
|
|
+**Location**: `web/apps/lq_label/src/components/config-preview/`
|
|
|
+
|
|
|
+**Responsibility**: 配置预览面板
|
|
|
+
|
|
|
+**Interface**:
|
|
|
+```typescript
|
|
|
+interface ConfigPreviewProps {
|
|
|
+ config: string;
|
|
|
+ data?: Record<string, any>;
|
|
|
+ onError?: (error: string) => void;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Features**:
|
|
|
+- LabelStudio 编辑器集成
|
|
|
+- 示例数据支持
|
|
|
+- 错误显示
|
|
|
+- 全屏模式
|
|
|
+
|
|
|
+#### 7. DataExportDialog
|
|
|
+**Location**: `web/apps/lq_label/src/components/data-export-dialog/`
|
|
|
+
|
|
|
+**Responsibility**: 数据导出对话框
|
|
|
+
|
|
|
+**Interface**:
|
|
|
+```typescript
|
|
|
+interface DataExportDialogProps {
|
|
|
+ open: boolean;
|
|
|
+ projectId: string;
|
|
|
+ onClose: () => void;
|
|
|
+}
|
|
|
+
|
|
|
+type ExportFormat = 'json' | 'csv' | 'coco' | 'yolo';
|
|
|
+
|
|
|
+interface ExportOptions {
|
|
|
+ format: ExportFormat;
|
|
|
+ status_filter?: 'all' | 'completed' | 'pending' | 'in_progress';
|
|
|
+ include_metadata: boolean;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Features**:
|
|
|
+- 格式选择
|
|
|
+- 状态筛选
|
|
|
+- 进度显示
|
|
|
+- 下载触发
|
|
|
+
|
|
|
+#### 8. ProjectStatisticsPanel
|
|
|
+**Location**: `web/apps/lq_label/src/components/project-statistics-panel/`
|
|
|
+
|
|
|
+**Responsibility**: 项目统计面板
|
|
|
+
|
|
|
+**Interface**:
|
|
|
+```typescript
|
|
|
+interface ProjectStatisticsPanelProps {
|
|
|
+ projectId: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface ProjectStatistics {
|
|
|
+ total_tasks: number;
|
|
|
+ completed_tasks: number;
|
|
|
+ in_progress_tasks: number;
|
|
|
+ pending_tasks: number;
|
|
|
+ total_items: number;
|
|
|
+ annotated_items: number;
|
|
|
+ completion_rate: number;
|
|
|
+ user_stats: UserTaskStats[];
|
|
|
+}
|
|
|
+
|
|
|
+interface UserTaskStats {
|
|
|
+ user_id: string;
|
|
|
+ username: string;
|
|
|
+ assigned_tasks: number;
|
|
|
+ completed_tasks: number;
|
|
|
+ annotation_count: number;
|
|
|
+ completion_rate: number;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Features**:
|
|
|
+- 项目级统计
|
|
|
+- 人员级统计
|
|
|
+- 进度条可视化
|
|
|
+- 实时更新
|
|
|
+
|
|
|
+### Backend API Endpoints
|
|
|
+
|
|
|
+#### User API Extensions
|
|
|
+**Router**: `backend/routers/user.py`
|
|
|
+
|
|
|
+**New Endpoints**:
|
|
|
+```python
|
|
|
+GET /api/users # List all users (admin only)
|
|
|
+GET /api/users/{id} # Get user by ID
|
|
|
+GET /api/users/{id}/stats # Get user task statistics
|
|
|
+GET /api/users/annotators # List annotators for assignment
|
|
|
+```
|
|
|
+
|
|
|
+**Models**:
|
|
|
+```python
|
|
|
+class UserResponse(BaseModel):
|
|
|
+ id: str
|
|
|
+ username: str
|
|
|
+ email: str
|
|
|
+ role: str
|
|
|
+ created_at: datetime
|
|
|
+
|
|
|
+class UserWithStatsResponse(UserResponse):
|
|
|
+ task_stats: TaskStats
|
|
|
+
|
|
|
+class TaskStats(BaseModel):
|
|
|
+ assigned_count: int
|
|
|
+ completed_count: int
|
|
|
+ annotation_count: int
|
|
|
+ completion_rate: float
|
|
|
+```
|
|
|
+
|
|
|
+#### Task Assignment API
|
|
|
+**Router**: `backend/routers/task.py` (扩展)
|
|
|
+
|
|
|
+**New Endpoints**:
|
|
|
+```python
|
|
|
+PUT /api/tasks/{id}/assign # Assign task to user
|
|
|
+POST /api/tasks/batch-assign # Batch assign tasks
|
|
|
+GET /api/tasks/my-tasks # Get current user's tasks
|
|
|
+```
|
|
|
+
|
|
|
+**Models**:
|
|
|
+```python
|
|
|
+class TaskAssignRequest(BaseModel):
|
|
|
+ user_id: str
|
|
|
+
|
|
|
+class BatchAssignRequest(BaseModel):
|
|
|
+ task_ids: List[str]
|
|
|
+ user_ids: List[str]
|
|
|
+ mode: str = 'round_robin' # 'round_robin' | 'equal'
|
|
|
+```
|
|
|
+
|
|
|
+#### Template API
|
|
|
+**Router**: `backend/routers/template.py`
|
|
|
+
|
|
|
+**Endpoints**:
|
|
|
+```python
|
|
|
+GET /api/templates # List all templates
|
|
|
+GET /api/templates/{id} # Get template by ID
|
|
|
+GET /api/templates/categories # List template categories
|
|
|
+POST /api/templates/validate # Validate XML config
|
|
|
+```
|
|
|
+
|
|
|
+**Models**:
|
|
|
+```python
|
|
|
+class TemplateResponse(BaseModel):
|
|
|
+ id: str
|
|
|
+ name: str
|
|
|
+ category: str
|
|
|
+ description: str
|
|
|
+ config: str
|
|
|
+ preview_image: Optional[str]
|
|
|
+ tags: List[str]
|
|
|
+
|
|
|
+class ConfigValidationRequest(BaseModel):
|
|
|
+ config: str
|
|
|
+
|
|
|
+class ConfigValidationResponse(BaseModel):
|
|
|
+ valid: bool
|
|
|
+ errors: List[ValidationError]
|
|
|
+```
|
|
|
+
|
|
|
+#### Export API
|
|
|
+**Router**: `backend/routers/export.py`
|
|
|
+
|
|
|
+**Endpoints**:
|
|
|
+```python
|
|
|
+POST /api/projects/{id}/export # Export project annotations
|
|
|
+GET /api/exports/{id}/status # Get export job status
|
|
|
+GET /api/exports/{id}/download # Download export file
|
|
|
+```
|
|
|
+
|
|
|
+**Models**:
|
|
|
+```python
|
|
|
+class ExportRequest(BaseModel):
|
|
|
+ format: str # 'json' | 'csv' | 'coco' | 'yolo'
|
|
|
+ status_filter: Optional[str]
|
|
|
+ include_metadata: bool = True
|
|
|
+
|
|
|
+class ExportResponse(BaseModel):
|
|
|
+ export_id: str
|
|
|
+ status: str
|
|
|
+ download_url: Optional[str]
|
|
|
+```
|
|
|
+
|
|
|
+#### Statistics API
|
|
|
+**Router**: `backend/routers/statistics.py`
|
|
|
+
|
|
|
+**Endpoints**:
|
|
|
+```python
|
|
|
+GET /api/projects/{id}/statistics # Get project statistics
|
|
|
+GET /api/statistics/overview # Get overall platform statistics
|
|
|
+```
|
|
|
+
|
|
|
+**Models**:
|
|
|
+```python
|
|
|
+class ProjectStatisticsResponse(BaseModel):
|
|
|
+ total_tasks: int
|
|
|
+ completed_tasks: int
|
|
|
+ in_progress_tasks: int
|
|
|
+ pending_tasks: int
|
|
|
+ total_items: int
|
|
|
+ annotated_items: int
|
|
|
+ completion_rate: float
|
|
|
+ user_stats: List[UserTaskStats]
|
|
|
+```
|
|
|
+
|
|
|
+## Data Models
|
|
|
+
|
|
|
+### Database Schema Extensions
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 预设模板表 (可选,也可以使用静态配置)
|
|
|
+CREATE TABLE templates (
|
|
|
+ id TEXT PRIMARY KEY,
|
|
|
+ name TEXT NOT NULL,
|
|
|
+ category TEXT NOT NULL,
|
|
|
+ description TEXT,
|
|
|
+ config TEXT NOT NULL,
|
|
|
+ preview_image TEXT,
|
|
|
+ tags TEXT, -- JSON array
|
|
|
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
|
+);
|
|
|
+
|
|
|
+-- 导出任务表
|
|
|
+CREATE TABLE export_jobs (
|
|
|
+ id TEXT PRIMARY KEY,
|
|
|
+ project_id TEXT NOT NULL,
|
|
|
+ format TEXT NOT NULL,
|
|
|
+ status TEXT DEFAULT 'pending', -- pending, processing, completed, failed
|
|
|
+ file_path TEXT,
|
|
|
+ error_message TEXT,
|
|
|
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ completed_at TIMESTAMP,
|
|
|
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
|
+);
|
|
|
+
|
|
|
+-- 任务分配历史表 (可选,用于追踪分配记录)
|
|
|
+CREATE TABLE task_assignments (
|
|
|
+ id TEXT PRIMARY KEY,
|
|
|
+ task_id TEXT NOT NULL,
|
|
|
+ assigned_to TEXT NOT NULL,
|
|
|
+ assigned_by TEXT NOT NULL,
|
|
|
+ assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
|
|
|
+ FOREIGN KEY (assigned_to) REFERENCES users(id),
|
|
|
+ FOREIGN KEY (assigned_by) REFERENCES users(id)
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+### State Management (Jotai Atoms)
|
|
|
+
|
|
|
+**Location**: `web/apps/lq_label/src/atoms/`
|
|
|
+
|
|
|
+```typescript
|
|
|
+// userAtoms.ts
|
|
|
+export const usersAtom = atom<UserWithStats[]>([]);
|
|
|
+export const annotatorsAtom = atom<AssignableUser[]>([]);
|
|
|
+export const userLoadingAtom = atom<boolean>(false);
|
|
|
+
|
|
|
+// templateAtoms.ts
|
|
|
+export const templatesAtom = atom<Template[]>([]);
|
|
|
+export const selectedTemplateAtom = atom<Template | null>(null);
|
|
|
+export const templateCategoriesAtom = atom<TemplateCategory[]>([]);
|
|
|
+
|
|
|
+// configEditorAtoms.ts
|
|
|
+export const configValueAtom = atom<string>('');
|
|
|
+export const configValidationAtom = atom<ValidationResult | null>(null);
|
|
|
+export const editorModeAtom = atom<'code' | 'visual'>('code');
|
|
|
+
|
|
|
+// statisticsAtoms.ts
|
|
|
+export const projectStatisticsAtom = atom<ProjectStatistics | null>(null);
|
|
|
+export const statisticsLoadingAtom = atom<boolean>(false);
|
|
|
+
|
|
|
+// exportAtoms.ts
|
|
|
+export const exportJobAtom = atom<ExportJob | null>(null);
|
|
|
+export const exportProgressAtom = atom<number>(0);
|
|
|
+```
|
|
|
+
|
|
|
+### Predefined Templates
|
|
|
+
|
|
|
+模板将以静态配置的形式存储在前端:
|
|
|
+
|
|
|
+```typescript
|
|
|
+// templates/index.ts
|
|
|
+export const PREDEFINED_TEMPLATES: Template[] = [
|
|
|
+ {
|
|
|
+ id: 'image_classification_basic',
|
|
|
+ name: '图像分类 - 基础',
|
|
|
+ category: 'image_classification',
|
|
|
+ description: '简单的图像分类任务,支持单选分类',
|
|
|
+ config: `<View>
|
|
|
+ <Image name="image" value="$image"/>
|
|
|
+ <Choices name="choice" toName="image">
|
|
|
+ <Choice value="类别1"/>
|
|
|
+ <Choice value="类别2"/>
|
|
|
+ <Choice value="类别3"/>
|
|
|
+ </Choices>
|
|
|
+</View>`,
|
|
|
+ tags: ['图像', '分类', '单选'],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'object_detection_bbox',
|
|
|
+ name: '目标检测 - 边界框',
|
|
|
+ category: 'object_detection',
|
|
|
+ description: '使用矩形框标注图像中的目标',
|
|
|
+ config: `<View>
|
|
|
+ <Image name="image" value="$image"/>
|
|
|
+ <RectangleLabels name="label" toName="image">
|
|
|
+ <Label value="目标1"/>
|
|
|
+ <Label value="目标2"/>
|
|
|
+ </RectangleLabels>
|
|
|
+</View>`,
|
|
|
+ tags: ['图像', '检测', '边界框'],
|
|
|
+ },
|
|
|
+ // ... 更多模板
|
|
|
+];
|
|
|
+```
|
|
|
+
|
|
|
+</text>
|
|
|
+</invoke>
|
|
|
+
|
|
|
+
|
|
|
+## Correctness Properties
|
|
|
+
|
|
|
+*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
|
|
+
|
|
|
+### Property 1: 用户列表筛选正确性
|
|
|
+*For any* 角色筛选值或搜索关键词,返回的用户列表中的所有用户都应该满足筛选条件:
|
|
|
+- 如果按角色筛选,所有用户的 role 字段都应该等于筛选值
|
|
|
+- 如果按关键词搜索,所有用户的 username 或 email 都应该包含该关键词
|
|
|
+
|
|
|
+**Validates: Requirements 1.2, 1.3**
|
|
|
+
|
|
|
+### Property 2: 任务分配正确性
|
|
|
+*For any* 任务和用户的分配操作:
|
|
|
+- 分配后任务的 assigned_to 字段应该等于选择的用户 ID
|
|
|
+- 分配记录应该包含正确的分配时间和分配人信息
|
|
|
+
|
|
|
+**Validates: Requirements 2.3, 2.4**
|
|
|
+
|
|
|
+### Property 3: 批量分配平均性
|
|
|
+*For any* 任务列表和用户列表的批量分配操作(平均分配模式),分配后每个用户分配的任务数量差异不应超过 1。
|
|
|
+
|
|
|
+**Validates: Requirements 2.6**
|
|
|
+
|
|
|
+### Property 4: 标注人员任务可见性
|
|
|
+*For any* 标注人员用户,其任务列表中的所有任务的 assigned_to 字段都应该等于该用户的 ID。任何状态筛选都应该在此基础上进行。
|
|
|
+
|
|
|
+**Validates: Requirements 3.1, 3.2**
|
|
|
+
|
|
|
+### Property 5: 任务状态自动更新
|
|
|
+*For any* 任务,当其所有数据项都被标注后,任务状态应该自动更新为 'completed'。
|
|
|
+
|
|
|
+**Validates: Requirements 3.6**
|
|
|
+
|
|
|
+### Property 6: 模板选择填充
|
|
|
+*For any* 选择的模板,确认选择后项目配置字段的值应该等于模板的 config 值。模板搜索结果中的每个模板的名称或描述都应该包含搜索关键词。
|
|
|
+
|
|
|
+**Validates: Requirements 4.4, 4.5**
|
|
|
+
|
|
|
+### Property 7: XML 配置验证
|
|
|
+*For any* XML 配置字符串,验证结果应该正确反映其语法有效性:
|
|
|
+- 有效的 XML 应该返回 valid: true
|
|
|
+- 无效的 XML 应该返回 valid: false 并包含错误位置信息
|
|
|
+
|
|
|
+**Validates: Requirements 5.3, 5.4**
|
|
|
+
|
|
|
+### Property 8: 配置编辑器同步 (Round-trip)
|
|
|
+*For any* 有效的标注配置:
|
|
|
+- 在代码模式和可视化模式之间切换后,配置内容应该保持语义等价
|
|
|
+- 导出配置后再导入应该得到语义等价的配置
|
|
|
+
|
|
|
+**Validates: Requirements 5.6, 5.8**
|
|
|
+
|
|
|
+### Property 9: 可视化编辑器 XML 生成
|
|
|
+*For any* 组件拖拽或属性修改操作,生成的 XML 代码应该正确反映该操作:
|
|
|
+- 拖拽组件后,XML 应该包含该组件的标签
|
|
|
+- 修改属性后,XML 中对应的属性值应该更新
|
|
|
+
|
|
|
+**Validates: Requirements 6.3, 6.6**
|
|
|
+
|
|
|
+### Property 10: 配置预览错误处理
|
|
|
+*For any* 无效的标注配置,预览面板应该显示错误信息而不是崩溃或显示空白。
|
|
|
+
|
|
|
+**Validates: Requirements 7.5**
|
|
|
+
|
|
|
+### Property 11: 数据导出完整性
|
|
|
+*For any* 导出请求:
|
|
|
+- 导出的数据应该只包含匹配状态筛选条件的任务
|
|
|
+- 导出文件应该包含完整的任务元数据和标注结果
|
|
|
+- 导出格式应该符合所选格式的规范(JSON/CSV/COCO/YOLO)
|
|
|
+
|
|
|
+**Validates: Requirements 8.3, 8.4, 8.5**
|
|
|
+
|
|
|
+### Property 12: 统计数据准确性
|
|
|
+*For any* 项目或用户:
|
|
|
+- 项目统计应该准确反映实际的任务数量和状态分布
|
|
|
+- 用户统计应该准确反映该用户的任务分配和完成情况
|
|
|
+- 进度百分比应该等于 (已标注数 / 总数) * 100
|
|
|
+- 标注操作后统计数据应该立即更新
|
|
|
+
|
|
|
+**Validates: Requirements 9.2, 9.3, 9.6, 9.7**
|
|
|
+
|
|
|
+## Error Handling
|
|
|
+
|
|
|
+### Frontend Error Handling
|
|
|
+
|
|
|
+1. **API 请求错误**
|
|
|
+ - 使用 try-catch 包装所有 API 调用
|
|
|
+ - 显示用户友好的错误消息
|
|
|
+ - 记录错误到控制台
|
|
|
+
|
|
|
+2. **配置验证错误**
|
|
|
+ - 实时显示 XML 语法错误
|
|
|
+ - 高亮错误位置
|
|
|
+ - 提供修复建议
|
|
|
+
|
|
|
+3. **导出错误**
|
|
|
+ - 显示导出失败原因
|
|
|
+ - 支持重试
|
|
|
+ - 提供错误日志下载
|
|
|
+
|
|
|
+4. **编辑器错误**
|
|
|
+ - Monaco Editor 加载失败时显示降级方案
|
|
|
+ - LabelStudio 预览失败时显示错误信息
|
|
|
+
|
|
|
+### Backend Error Handling
|
|
|
+
|
|
|
+1. **权限错误**
|
|
|
+ - 非管理员访问用户管理返回 403
|
|
|
+ - 标注人员访问非分配任务返回 403
|
|
|
+
|
|
|
+2. **数据验证错误**
|
|
|
+ - 无效的 XML 配置返回 400
|
|
|
+ - 无效的导出格式返回 400
|
|
|
+
|
|
|
+3. **资源不存在**
|
|
|
+ - 用户/任务/项目不存在返回 404
|
|
|
+
|
|
|
+4. **导出错误**
|
|
|
+ - 导出失败记录错误日志
|
|
|
+ - 返回失败状态和错误信息
|
|
|
+
|
|
|
+## Testing Strategy
|
|
|
+
|
|
|
+### Unit Testing
|
|
|
+
|
|
|
+**Frontend**:
|
|
|
+- 测试组件渲染和交互
|
|
|
+- 测试状态管理逻辑
|
|
|
+- 测试 XML 验证函数
|
|
|
+- 测试统计计算函数
|
|
|
+
|
|
|
+**Backend**:
|
|
|
+- 测试 API 端点
|
|
|
+- 测试数据库操作
|
|
|
+- 测试导出格式转换
|
|
|
+- 测试权限检查
|
|
|
+
|
|
|
+### Property-Based Testing
|
|
|
+
|
|
|
+**配置**:
|
|
|
+- 前端使用 fast-check
|
|
|
+- 后端使用 Hypothesis
|
|
|
+- 每个属性测试至少运行 100 次迭代
|
|
|
+- 使用 **Feature: task-assignment-template, Property N: {property_text}** 格式标记测试
|
|
|
+
|
|
|
+**重点属性测试**:
|
|
|
+- Property 1: 用户列表筛选
|
|
|
+- Property 3: 批量分配平均性
|
|
|
+- Property 4: 标注人员任务可见性
|
|
|
+- Property 7: XML 配置验证
|
|
|
+- Property 8: 配置编辑器同步
|
|
|
+- Property 11: 数据导出完整性
|
|
|
+- Property 12: 统计数据准确性
|
|
|
+
|
|
|
+### Integration Testing
|
|
|
+
|
|
|
+- 测试完整的任务分配流程
|
|
|
+- 测试模板选择到项目创建流程
|
|
|
+- 测试配置编辑到预览流程
|
|
|
+- 测试数据导出流程
|
|
|
+
|
|
|
+## Implementation Notes
|
|
|
+
|
|
|
+### Monaco Editor Integration
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 安装依赖
|
|
|
+// npm install @monaco-editor/react
|
|
|
+
|
|
|
+import Editor from '@monaco-editor/react';
|
|
|
+
|
|
|
+const ConfigCodeEditor: React.FC<Props> = ({ value, onChange }) => {
|
|
|
+ return (
|
|
|
+ <Editor
|
|
|
+ height="400px"
|
|
|
+ language="xml"
|
|
|
+ value={value}
|
|
|
+ onChange={(value) => onChange(value || '')}
|
|
|
+ options={{
|
|
|
+ minimap: { enabled: false },
|
|
|
+ lineNumbers: 'on',
|
|
|
+ wordWrap: 'on',
|
|
|
+ automaticLayout: true,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ );
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+### XML Validation
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 使用 DOMParser 进行 XML 验证
|
|
|
+function validateXML(xml: string): ValidationResult {
|
|
|
+ const parser = new DOMParser();
|
|
|
+ const doc = parser.parseFromString(xml, 'application/xml');
|
|
|
+ const parseError = doc.querySelector('parsererror');
|
|
|
+
|
|
|
+ if (parseError) {
|
|
|
+ return {
|
|
|
+ valid: false,
|
|
|
+ errors: [{
|
|
|
+ line: 1,
|
|
|
+ column: 1,
|
|
|
+ message: parseError.textContent || 'XML 解析错误',
|
|
|
+ }],
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return { valid: true, errors: [] };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Export Format Converters
|
|
|
+
|
|
|
+```python
|
|
|
+# backend/services/export_service.py
|
|
|
+
|
|
|
+def export_to_json(annotations: List[Annotation]) -> str:
|
|
|
+ return json.dumps([a.to_dict() for a in annotations], indent=2)
|
|
|
+
|
|
|
+def export_to_csv(annotations: List[Annotation]) -> str:
|
|
|
+ # 使用 pandas 或 csv 模块
|
|
|
+ pass
|
|
|
+
|
|
|
+def export_to_coco(annotations: List[Annotation]) -> str:
|
|
|
+ # COCO 格式转换
|
|
|
+ pass
|
|
|
+
|
|
|
+def export_to_yolo(annotations: List[Annotation]) -> str:
|
|
|
+ # YOLO 格式转换
|
|
|
+ pass
|
|
|
+```
|
|
|
+
|
|
|
+### Statistics Calculation
|
|
|
+
|
|
|
+```python
|
|
|
+# backend/services/statistics_service.py
|
|
|
+
|
|
|
+def calculate_project_statistics(project_id: str) -> ProjectStatistics:
|
|
|
+ with get_db_connection() as conn:
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 任务统计
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT
|
|
|
+ COUNT(*) as total,
|
|
|
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
|
|
|
+ SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
|
|
|
+ SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending
|
|
|
+ FROM tasks WHERE project_id = ?
|
|
|
+ """, (project_id,))
|
|
|
+
|
|
|
+ # 标注统计
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT COUNT(*) as total_annotations
|
|
|
+ FROM annotations a
|
|
|
+ JOIN tasks t ON a.task_id = t.id
|
|
|
+ WHERE t.project_id = ?
|
|
|
+ """, (project_id,))
|
|
|
+
|
|
|
+ # 用户统计
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT
|
|
|
+ u.id, u.username,
|
|
|
+ COUNT(DISTINCT t.id) as assigned_tasks,
|
|
|
+ SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) as completed_tasks,
|
|
|
+ COUNT(a.id) as annotation_count
|
|
|
+ FROM users u
|
|
|
+ LEFT JOIN tasks t ON t.assigned_to = u.id AND t.project_id = ?
|
|
|
+ LEFT JOIN annotations a ON a.task_id = t.id AND a.user_id = u.id
|
|
|
+ WHERE u.role = 'annotator'
|
|
|
+ GROUP BY u.id, u.username
|
|
|
+ """, (project_id,))
|
|
|
+
|
|
|
+ # 返回统计结果
|
|
|
+ pass
|
|
|
+```
|
|
|
+
|
|
|
+## Deployment Considerations
|
|
|
+
|
|
|
+### Frontend
|
|
|
+
|
|
|
+- Monaco Editor 需要额外的 webpack 配置
|
|
|
+- 确保 @humansignal/editor 正确集成
|
|
|
+- 配置 CSS 前缀处理
|
|
|
+
|
|
|
+### Backend
|
|
|
+
|
|
|
+- 导出大文件时考虑使用异步任务队列
|
|
|
+- 统计查询可能需要缓存优化
|
|
|
+- 考虑添加导出文件的定期清理
|
|
|
+
|
|
|
+### Database
|
|
|
+
|
|
|
+- 添加必要的索引优化查询性能
|
|
|
+- 考虑统计数据的物化视图
|