feat: add all application source code

- apps/api: FastAPI backend with Dockerfile
- apps/web: Next.js frontend with Dockerfile
- apps/sensor: Signal collection agent
- packages: shared packages

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-22 18:57:44 +08:00
parent a840bf975b
commit 196d269b92
245 changed files with 42207 additions and 6 deletions

View File

@@ -0,0 +1,229 @@
"""
Redis Client - AWOOOI 分散式狀態儲存
=====================================
Phase 6.1.1: Multi-Sig Redis 遷移
Features:
- 非同步連線池 (Connection Pool)
- Lifespan 管理 (啟動/關閉)
- 分散式鎖 (Distributed Lock)
- 環境變數驅動 (禁止硬編碼 IP)
統帥鐵律:
- 所有 Redis 操作必須使用此模組
- 禁止在其他地方直接建立 Redis 連線
"""
import asyncio
from contextlib import asynccontextmanager
from typing import AsyncGenerator
import redis.asyncio as redis
import structlog
from src.core.config import settings
logger = structlog.get_logger(__name__)
# =============================================================================
# Connection Pool
# =============================================================================
_redis_pool: redis.Redis | None = None
async def init_redis_pool() -> redis.Redis:
"""
初始化 Redis 連線池
統帥鐵律: 連線池在 Lifespan 啟動時建立
"""
global _redis_pool
if _redis_pool is not None:
return _redis_pool
_redis_pool = redis.from_url(
settings.REDIS_URL,
encoding="utf-8",
decode_responses=True,
max_connections=20,
socket_timeout=5.0,
socket_connect_timeout=5.0,
)
# 測試連線
try:
await _redis_pool.ping()
logger.info(
"redis_pool_initialized",
url=settings.REDIS_URL.split("@")[-1], # 隱藏密碼
)
except redis.ConnectionError as e:
logger.error("redis_connection_failed", error=str(e))
raise
return _redis_pool
async def close_redis_pool() -> None:
"""
關閉 Redis 連線池
統帥鐵律: 連線池在 Lifespan 關閉時回收
"""
global _redis_pool
if _redis_pool is not None:
await _redis_pool.close()
_redis_pool = None
logger.info("redis_pool_closed")
def get_redis() -> redis.Redis:
"""
取得 Redis 連線
Raises:
RuntimeError: 若連線池未初始化
"""
if _redis_pool is None:
raise RuntimeError("Redis pool not initialized. Call init_redis_pool() first.")
return _redis_pool
# =============================================================================
# Distributed Lock (分散式鎖)
# =============================================================================
class RedisLock:
"""
Redis 分散式鎖
防禦場景:
- 防止 Web + Telegram 同時簽核導致 Race Condition
- 防止 K8s Executor 被觸發兩次
使用方式:
async with RedisLock("approval:123:lock", timeout=10):
# Critical section
await execute_approval()
"""
def __init__(
self,
key: str,
timeout: int = 30,
blocking_timeout: float = 5.0,
):
"""
Args:
key: 鎖的 Redis Key
timeout: 鎖的自動過期時間 (秒)
blocking_timeout: 等待取得鎖的最大時間 (秒)
"""
self.key = f"lock:{key}"
self.timeout = timeout
self.blocking_timeout = blocking_timeout
self._lock_value: str | None = None
async def acquire(self) -> bool:
"""
嘗試取得鎖
Returns:
bool: 是否成功取得鎖
"""
import uuid
redis_client = get_redis()
self._lock_value = str(uuid.uuid4())
# 使用 SET NX EX 實現原子操作
acquired = await redis_client.set(
self.key,
self._lock_value,
nx=True, # Only set if not exists
ex=self.timeout, # Expire in timeout seconds
)
if acquired:
logger.debug("redis_lock_acquired", key=self.key)
return True
# 如果沒有立即取得,則等待
start_time = asyncio.get_event_loop().time()
while asyncio.get_event_loop().time() - start_time < self.blocking_timeout:
await asyncio.sleep(0.1)
acquired = await redis_client.set(
self.key,
self._lock_value,
nx=True,
ex=self.timeout,
)
if acquired:
logger.debug("redis_lock_acquired_after_wait", key=self.key)
return True
logger.warning("redis_lock_timeout", key=self.key)
return False
async def release(self) -> bool:
"""
釋放鎖
使用 Lua Script 確保只釋放自己持有的鎖 (防止誤刪)
Returns:
bool: 是否成功釋放
"""
if self._lock_value is None:
return False
redis_client = get_redis()
# Lua script: 只有當值匹配時才刪除 (原子操作)
lua_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
result = await redis_client.eval(lua_script, 1, self.key, self._lock_value)
if result:
logger.debug("redis_lock_released", key=self.key)
return True
else:
logger.warning("redis_lock_release_failed", key=self.key)
return False
async def __aenter__(self) -> "RedisLock":
acquired = await self.acquire()
if not acquired:
raise RuntimeError(f"Failed to acquire lock: {self.key}")
return self
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
await self.release()
# =============================================================================
# Context Manager
# =============================================================================
@asynccontextmanager
async def redis_context() -> AsyncGenerator[redis.Redis, None]:
"""
Redis 連線 Context Manager
用於需要獨立連線的場景
"""
client = get_redis()
try:
yield client
finally:
pass # 使用連線池,不需要關閉