Files
PROJECT-CONTORL/openspec/changes/archive/2025-12-28-add-resource-workload/design.md
beabigegg 61fe01cb6b feat: implement workload heatmap module
- Backend (FastAPI):
  - Workload heatmap API with load level calculation
  - User workload detail endpoint with task breakdown
  - Redis caching for workload calculations (1hr TTL)
  - Department isolation and access control
  - WorkloadSnapshot model for historical data
  - Alembic migration for workload_snapshots table

- API Endpoints:
  - GET /api/workload/heatmap - Team workload overview
  - GET /api/workload/user/{id} - User workload detail
  - GET /api/workload/me - Current user workload

- Load Levels:
  - normal: <80%, warning: 80-99%, overloaded: >=100%

- Tests:
  - 26 unit/API tests
  - 15 E2E automated tests
  - 77 total tests passing

- OpenSpec:
  - add-resource-workload change archived
  - resource-management spec updated

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 01:13:21 +08:00

242 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Design: add-resource-workload
## Architecture Overview
```
┌─────────────┐ ┌─────────────────────┐ ┌───────────────┐
│ Frontend │────▶│ Workload API │────▶│ MySQL │
│ (Heatmap) │ │ /api/v1/workload │ │ (Snapshots) │
└─────────────┘ └─────────────────────┘ └───────────────┘
┌───────────────┐
│ Redis │
│ (Cache) │
└───────────────┘
```
## Key Design Decisions
### 1. 負載計算策略:即時計算 vs 快照
**決定**:採用**混合策略**
- **即時計算**API 請求時計算,結果快取 1 小時
- **快照儲存**:每日凌晨儲存歷史快照供趨勢分析
**理由**
- 即時計算確保數據新鮮度
- 快照提供歷史趨勢分析能力
- Redis 快取減少計算負擔
### 2. 週邊界定義
**決定**:採用 **ISO 8601 週**(週一至週日)
**理由**
- 國際標準,避免歧義
- Python/MySQL 原生支援
- 便於未來國際化
### 3. 負載計算公式
```
週負載 = Σ(該週到期任務的 original_estimate) / 週容量 × 100%
```
**任務計入規則**
- `due_date` 在該週範圍內
- `assignee_id` 為目標使用者
- `status` 非已完成狀態
**邊界情況處理**
- `original_estimate` 為空:計為 0不計入負載
- `capacity` 為 0顯示為 N/A避免除以零
### 4. 負載等級閾值
| 等級 | 範圍 | 顏色 | 描述 |
|------|------|------|------|
| normal | 0-79% | green | 正常 |
| warning | 80-99% | yellow | 警告 |
| overloaded | ≥100% | red | 超載 |
### 5. 快取策略
```
快取鍵格式workload:{user_id}:{week_start}
TTL3600 秒1 小時)
```
**失效時機**
- 任務建立/更新/刪除
- 使用者容量變更
**實作**:暫不實作主動失效,依賴 TTL 自然過期Phase 1 簡化)
### 6. 權限控制
| 角色 | 可查看範圍 |
|------|-----------|
| super_admin | 所有使用者 |
| manager | 同部門使用者 |
| engineer | 僅自己 |
## API Design
### GET /api/v1/workload/heatmap
查詢團隊負載熱圖
**Query Parameters**:
- `week_start`: ISO 日期(預設當週一)
- `department_id`: 部門篩選(可選)
- `user_ids`: 使用者 ID 陣列(可選)
**Response**:
```json
{
"week_start": "2024-01-01",
"week_end": "2024-01-07",
"users": [
{
"user_id": "uuid",
"user_name": "John Doe",
"department_id": "uuid",
"department_name": "R&D",
"capacity_hours": 40.0,
"allocated_hours": 32.5,
"load_percentage": 81.25,
"load_level": "warning",
"task_count": 5
}
]
}
```
### GET /api/v1/workload/user/{user_id}
查詢特定使用者的負載詳情
**Query Parameters**:
- `week_start`: ISO 日期
**Response**:
```json
{
"user_id": "uuid",
"week_start": "2024-01-01",
"capacity_hours": 40.0,
"allocated_hours": 32.5,
"load_percentage": 81.25,
"load_level": "warning",
"tasks": [
{
"task_id": "uuid",
"title": "Task Name",
"project_name": "Project A",
"due_date": "2024-01-05",
"original_estimate": 8.0,
"status": "in_progress"
}
]
}
```
### PUT /api/v1/users/{user_id}/capacity
更新使用者容量
**Request Body**:
```json
{
"capacity": 32.0,
"effective_from": "2024-01-08",
"effective_until": "2024-01-14",
"reason": "年假"
}
```
## Data Model
### pjctrl_workload_snapshots
儲存歷史負載快照
```sql
CREATE TABLE pjctrl_workload_snapshots (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
week_start DATE NOT NULL,
allocated_hours DECIMAL(8,2) NOT NULL DEFAULT 0,
capacity_hours DECIMAL(8,2) NOT NULL DEFAULT 40,
load_percentage DECIMAL(5,2) NOT NULL DEFAULT 0,
task_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES pjctrl_users(id) ON DELETE CASCADE,
UNIQUE KEY uk_user_week (user_id, week_start),
INDEX idx_week_start (week_start)
);
```
### pjctrl_capacity_adjustments可選Phase 1 暫不實作)
儲存臨時容量調整(如請假)
```sql
CREATE TABLE pjctrl_capacity_adjustments (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
week_start DATE NOT NULL,
adjusted_capacity DECIMAL(5,2) NOT NULL,
reason VARCHAR(200),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES pjctrl_users(id) ON DELETE CASCADE,
UNIQUE KEY uk_user_week (user_id, week_start)
);
```
## Implementation Notes
### 後端結構
```
backend/app/
├── api/
│ └── workload/
│ ├── __init__.py
│ └── router.py
├── services/
│ └── workload_service.py
├── models/
│ └── workload_snapshot.py
└── schemas/
└── workload.py
```
### 週起始日計算
```python
from datetime import date, timedelta
def get_week_start(d: date) -> date:
"""取得 ISO 週的週一"""
return d - timedelta(days=d.weekday())
```
### Redis 快取範例
```python
async def get_user_workload(user_id: str, week_start: date) -> dict:
cache_key = f"workload:{user_id}:{week_start}"
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
result = calculate_workload(user_id, week_start)
redis_client.setex(cache_key, 3600, json.dumps(result))
return result
```