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:
188
app/services/notify_service.py
Normal file
188
app/services/notify_service.py
Normal 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,
|
||||
)
|
||||
Reference in New Issue
Block a user