Initial commit: KPI Management System Backend

Features:
- FastAPI backend with JWT authentication
- MySQL database with SQLAlchemy ORM
- KPI workflow: draft → pending → approved → evaluation → completed
- Ollama LLM API integration for AI features
- Gitea API integration for version control
- Complete API endpoints for KPI, dashboard, notifications

Tables: KPI_D_* prefix naming convention

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-11 16:20:57 +08:00
commit f810ddc2ea
48 changed files with 4950 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
"""
通知服務
"""
from datetime import datetime
from typing import Optional, List
from sqlalchemy.orm import Session
from app.models.notification import Notification, NotificationPreference
from app.models.employee import Employee
from app.models.kpi_sheet import KPISheet
class NotifyService:
"""通知服務"""
def __init__(self, db: Session):
self.db = db
def create(
self,
recipient_id: int,
type: str,
title: str,
content: Optional[str] = None,
related_sheet_id: Optional[int] = None,
) -> Notification:
"""建立通知"""
notification = Notification(
recipient_id=recipient_id,
type=type,
title=title,
content=content,
related_sheet_id=related_sheet_id,
)
self.db.add(notification)
self.db.commit()
self.db.refresh(notification)
return notification
def get_by_recipient(
self, recipient_id: int, skip: int = 0, limit: int = 50
) -> List[Notification]:
"""取得通知列表"""
return (
self.db.query(Notification)
.filter(Notification.recipient_id == recipient_id)
.order_by(Notification.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
def get_unread_count(self, recipient_id: int) -> int:
"""取得未讀數量"""
return (
self.db.query(Notification)
.filter(
Notification.recipient_id == recipient_id,
Notification.is_read == False,
)
.count()
)
def mark_as_read(self, notification_id: int, recipient_id: int) -> bool:
"""標記已讀"""
notification = (
self.db.query(Notification)
.filter(
Notification.id == notification_id,
Notification.recipient_id == recipient_id,
)
.first()
)
if not notification:
return False
notification.is_read = True
notification.read_at = datetime.utcnow()
self.db.commit()
return True
def mark_all_as_read(self, recipient_id: int) -> int:
"""全部標記已讀"""
result = (
self.db.query(Notification)
.filter(
Notification.recipient_id == recipient_id,
Notification.is_read == False,
)
.update({"is_read": True, "read_at": datetime.utcnow()})
)
self.db.commit()
return result
def get_preferences(self, employee_id: int) -> Optional[NotificationPreference]:
"""取得通知偏好"""
return (
self.db.query(NotificationPreference)
.filter(NotificationPreference.employee_id == employee_id)
.first()
)
def update_preferences(
self,
employee_id: int,
email_enabled: Optional[bool] = None,
in_app_enabled: Optional[bool] = None,
reminder_days_before: Optional[int] = None,
) -> NotificationPreference:
"""更新通知偏好"""
pref = self.get_preferences(employee_id)
if not pref:
pref = NotificationPreference(employee_id=employee_id)
self.db.add(pref)
if email_enabled is not None:
pref.email_enabled = email_enabled
if in_app_enabled is not None:
pref.in_app_enabled = in_app_enabled
if reminder_days_before is not None:
pref.reminder_days_before = reminder_days_before
self.db.commit()
self.db.refresh(pref)
return pref
# ==================== 事件通知 ====================
def notify_kpi_submitted(self, sheet: KPISheet) -> None:
"""通知 KPI 已提交"""
employee = sheet.employee
if not employee.manager_id:
return
self.create(
recipient_id=employee.manager_id,
type="kpi_submitted",
title="KPI 待審核",
content=f"{employee.name}{sheet.period.code} KPI 已提交,請審核。",
related_sheet_id=sheet.id,
)
def notify_kpi_approved(self, sheet: KPISheet) -> None:
"""通知 KPI 已核准"""
self.create(
recipient_id=sheet.employee_id,
type="kpi_approved",
title="KPI 已核准",
content=f"您的 {sheet.period.code} KPI 已審核通過。",
related_sheet_id=sheet.id,
)
def notify_kpi_rejected(self, sheet: KPISheet, reason: str) -> None:
"""通知 KPI 已退回"""
self.create(
recipient_id=sheet.employee_id,
type="kpi_rejected",
title="KPI 已退回",
content=f"您的 {sheet.period.code} KPI 已退回,原因:{reason}",
related_sheet_id=sheet.id,
)
def notify_self_eval_completed(self, sheet: KPISheet) -> None:
"""通知自評已完成"""
employee = sheet.employee
if not employee.manager_id:
return
self.create(
recipient_id=employee.manager_id,
type="self_eval_completed",
title="員工自評已完成",
content=f"{employee.name}{sheet.period.code} 自評已完成,請進行覆核。",
related_sheet_id=sheet.id,
)
def notify_manager_eval_completed(self, sheet: KPISheet) -> None:
"""通知主管評核已完成"""
self.create(
recipient_id=sheet.employee_id,
type="manager_eval_completed",
title="KPI 評核已完成",
content=f"您的 {sheet.period.code} KPI 評核已完成,獎金月數:{sheet.total_score}",
related_sheet_id=sheet.id,
)