feat(api): 統一使用台北時區 UTC+8 (禁止 UTC)

- 新增 src/utils/timezone.py 時區工具函式
- 修改 11 個後端檔案,全部改用 now_taipei()
- 更新 HARD_RULES.md 加入時區鐵律章節
- 更新 Skills 02/04 加入時區禁令

🔴 HARD RULE: 禁止 datetime.utcnow() / datetime.now(UTC)
 正確做法: from src.utils.timezone import now_taipei

Memory: feedback_timezone_taipei.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-25 09:08:34 +08:00
parent 5f3271174f
commit 2a2dac865a
14 changed files with 276 additions and 49 deletions

View File

@@ -14,7 +14,9 @@ Phase 3.3: 商業變現能力 - Day-1 ROI
import logging
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from datetime import timedelta
from src.utils.timezone import now_taipei
from enum import Enum
from typing import Literal
@@ -232,7 +234,7 @@ class IdleResourceScanner:
CostReport 包含所有浪費資源與建議動作
"""
self._scan_counter += 1
scan_id = f"scan-{self._scan_counter:04d}-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
scan_id = f"scan-{self._scan_counter:04d}-{now_taipei().strftime('%Y%m%d%H%M%S')}"
logger.info(f"[FinOps] Starting full scan: {scan_id}")
@@ -254,7 +256,7 @@ class IdleResourceScanner:
report = CostReport(
scan_id=scan_id,
scanned_at=datetime.utcnow(),
scanned_at=now_taipei(),
cluster_name=cluster_name,
total_wasted_usd=total_wasted,
total_resources_scanned=self._get_mock_total_resources(),
@@ -288,23 +290,23 @@ class IdleResourceScanner:
"namespace": "database",
"size_gb": 500,
"storage_class": "gp3",
"created": datetime.utcnow() - timedelta(days=90),
"last_used": datetime.utcnow() - timedelta(days=60),
"created": now_taipei() - timedelta(days=90),
"last_used": now_taipei() - timedelta(days=60),
},
{
"name": "logs-elasticsearch-2023",
"namespace": "logging",
"size_gb": 200,
"storage_class": "gp2",
"created": datetime.utcnow() - timedelta(days=180),
"last_used": datetime.utcnow() - timedelta(days=120),
"created": now_taipei() - timedelta(days=180),
"last_used": now_taipei() - timedelta(days=120),
},
{
"name": "cache-redis-temp",
"namespace": "default",
"size_gb": 50,
"storage_class": "gp3",
"created": datetime.utcnow() - timedelta(days=30),
"created": now_taipei() - timedelta(days=30),
"last_used": None,
},
]
@@ -348,8 +350,8 @@ class IdleResourceScanner:
"cpu_request": 2.0, # vCPU
"mem_request_gb": 4.0,
"avg_cpu_percent": 0.3,
"created": datetime.utcnow() - timedelta(days=120),
"last_active": datetime.utcnow() - timedelta(days=45),
"created": now_taipei() - timedelta(days=120),
"last_active": now_taipei() - timedelta(days=45),
},
{
"name": "test-worker-batch-xyz99",
@@ -357,8 +359,8 @@ class IdleResourceScanner:
"cpu_request": 1.0,
"mem_request_gb": 2.0,
"avg_cpu_percent": 0.1,
"created": datetime.utcnow() - timedelta(days=60),
"last_active": datetime.utcnow() - timedelta(days=30),
"created": now_taipei() - timedelta(days=60),
"last_active": now_taipei() - timedelta(days=30),
},
{
"name": "debug-shell-admin",
@@ -366,8 +368,8 @@ class IdleResourceScanner:
"cpu_request": 0.5,
"mem_request_gb": 1.0,
"avg_cpu_percent": 0.0,
"created": datetime.utcnow() - timedelta(days=14),
"last_active": datetime.utcnow() - timedelta(days=10),
"created": now_taipei() - timedelta(days=14),
"last_active": now_taipei() - timedelta(days=10),
},
]
@@ -420,7 +422,7 @@ class IdleResourceScanner:
"requested_mem_gb": 48.0,
"actual_cpu": 2.0,
"actual_mem_gb": 8.0,
"created": datetime.utcnow() - timedelta(days=200),
"created": now_taipei() - timedelta(days=200),
},
{
"name": "worker-gpu-unused",
@@ -431,7 +433,7 @@ class IdleResourceScanner:
"requested_mem_gb": 16.0,
"actual_cpu": 0.5,
"actual_mem_gb": 2.0,
"created": datetime.utcnow() - timedelta(days=90),
"created": now_taipei() - timedelta(days=90),
},
]
@@ -468,7 +470,7 @@ class IdleResourceScanner:
),
monthly_cost_usd=monthly_cost,
created_at=node["created"],
last_used_at=datetime.utcnow(),
last_used_at=now_taipei(),
spec={
"totalCpu": node["total_cpu"],
"totalMemoryGb": node["total_mem_gb"],