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

5.8 KiB
Raw Blame History

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:

{
  "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:

{
  "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:

{
  "capacity": 32.0,
  "effective_from": "2024-01-08",
  "effective_until": "2024-01-14",
  "reason": "年假"
}

Data Model

pjctrl_workload_snapshots

儲存歷史負載快照

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 暫不實作)

儲存臨時容量調整(如請假)

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

週起始日計算

from datetime import date, timedelta

def get_week_start(d: date) -> date:
    """取得 ISO 週的週一"""
    return d - timedelta(days=d.weekday())

Redis 快取範例

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