Files
JiuContinent 267b01b9f4 ```
feat(scheduler): 新增多种报表推送功能并重构调度器代码

- 新增考勤汇总、风险进度、工作日报、工作周报等报表推送功能
- 重构调度器中的报表推送逻辑,提取通用的 deliver_report 函数
- 添加新的定时任务配置项用于各种报表推送
- 实现飞书推送配置检查函数 _feishu_delivery_configured
- 将重复的报表推送代码抽取为可复用的函数模式
```
2026-07-13 09:50:09 +08:00

157 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import date
from decimal import Decimal
from typing import Any
from sqlalchemy import func, select
from app.modules.business.constants import (
ATTENDANCE_ABNORMAL_STATUSES,
PROJECT_CLOSED_STATUSES,
PENDING_APPROVAL_STATUSES,
StatusValue,
)
from app.modules.business.models import (
AttendanceRecord,
Expense,
FundAccount,
Procurement,
Project,
WorkTask,
)
from app.modules.reports.constants import (
ReportPushType,
ReportResponseKey,
ReportTitle,
)
from app.modules.risk.constants import RiskSummaryKey
from app.modules.reports.services.common import _money
class ReportSummaryMixin:
def daily_brief(self) -> dict:
project_count = self._count(Project)
task_count = self._count(WorkTask)
procurement_pending = self._count(
Procurement,
Procurement.approval_status.in_(PENDING_APPROVAL_STATUSES),
)
expense_pending = self._count(
Expense,
Expense.approval_status.in_(PENDING_APPROVAL_STATUSES),
)
fund_total = (
self.db.execute(select(func.sum(FundAccount.current_balance))).scalar()
or Decimal("0")
)
risk_summary = self.risks.summary()
attendance = self.attendance_summary()
lines = [
f"- 项目总数:{project_count}",
f"- 任务总数:{task_count}",
f"- 待处理采购:{procurement_pending}",
f"- 待处理费用:{expense_pending}",
f"- 当前账户总余额:{_money(fund_total)}",
(
f"- 今日打卡记录:{attendance[ReportResponseKey.TOTAL]}"
f"异常:{attendance[ReportResponseKey.ABNORMAL_TOTAL]}"
),
f"- 逾期任务:{len(risk_summary[RiskSummaryKey.OVERDUE_TASKS])}",
f"- 延期项目:{len(risk_summary[RiskSummaryKey.DELAYED_PROJECTS])}",
f"- 超预算项目:{len(risk_summary[RiskSummaryKey.OVER_BUDGET_PROJECTS])}",
f"- 资金风险账户:{len(risk_summary[RiskSummaryKey.FUND_RISKS])}",
f"- 供应商风险:{len(risk_summary[RiskSummaryKey.SUPPLIER_RISKS])}",
f"- 打开风险事件:{len(risk_summary[RiskSummaryKey.OPEN_EVENTS])}",
f"- 综合风险等级:{risk_summary[RiskSummaryKey.RISK_LEVEL]}",
]
return {
ReportResponseKey.TITLE: ReportTitle.DAILY_BRIEF,
ReportResponseKey.REPORT_TYPE: ReportPushType.DAILY_BRIEF,
ReportResponseKey.LINES: lines,
ReportResponseKey.CONTENT: "\n".join(lines),
}
def project_weekly(self) -> dict:
active = self._count(
Project,
Project.status.notin_(PROJECT_CLOSED_STATUSES),
)
delayed = self.risks.delayed_projects()
over_budget = self.risks.over_budget_projects()
open_risks = self.risks.list_events(status_filter=StatusValue.OPEN)
lines = [
f"- 活跃项目:{active}",
f"- 延期项目:{len(delayed)}",
f"- 超预算项目:{len(over_budget)}",
f"- 打开风险事件:{len(open_risks)}",
"- 需要管理层关注:",
]
for item in delayed[:10]:
lines.append(
f" - 延期:{item.get('code')} {item.get('name')}"
f"负责人 {item.get('owner')}"
)
for item in over_budget[:10]:
lines.append(f" - 超预算:{item.get('code')} {item.get('name')}")
return {
ReportResponseKey.TITLE: ReportTitle.PROJECT_WEEKLY,
ReportResponseKey.REPORT_TYPE: ReportPushType.PROJECT_WEEKLY,
ReportResponseKey.LINES: lines,
ReportResponseKey.CONTENT: "\n".join(lines),
}
def risk_progress(self) -> dict[str, Any]:
"""Build a standalone project risk progress report."""
summary = self.risks.summary()
lines = [
f"- 综合风险等级:{summary[RiskSummaryKey.RISK_LEVEL]}",
f"- 风险分:{summary[RiskSummaryKey.RISK_SCORE]}",
f"- 逾期任务:{len(summary[RiskSummaryKey.OVERDUE_TASKS])}",
f"- 延期项目:{len(summary[RiskSummaryKey.DELAYED_PROJECTS])}",
f"- 超预算项目:{len(summary[RiskSummaryKey.OVER_BUDGET_PROJECTS])}",
f"- 资金风险账户:{len(summary[RiskSummaryKey.FUND_RISKS])}",
f"- 供应商风险:{len(summary[RiskSummaryKey.SUPPLIER_RISKS])}",
f"- 打开风险事件:{len(summary[RiskSummaryKey.OPEN_EVENTS])}",
]
return {
ReportResponseKey.TITLE: ReportTitle.RISK_PROGRESS,
ReportResponseKey.REPORT_TYPE: ReportPushType.RISK_PROGRESS,
ReportResponseKey.LINES: lines,
ReportResponseKey.CONTENT: "\n".join(lines),
}
def attendance_summary(self, work_date: date | None = None) -> dict[str, Any]:
"""Summarize attendance records for one business day."""
target_date = work_date or date.today()
rows = self.db.execute(
select(AttendanceRecord.status, func.count())
.where(AttendanceRecord.work_date == target_date)
.group_by(AttendanceRecord.status)
).all()
status_counts = {str(status): int(count) for status, count in rows}
abnormal_total = sum(
count
for status, count in status_counts.items()
if status in ATTENDANCE_ABNORMAL_STATUSES
)
total = sum(status_counts.values())
lines = [
f"- 日期:{target_date.isoformat()}",
f"- 打卡记录:{total}",
f"- 异常记录:{abnormal_total}",
]
for status, count in sorted(status_counts.items()):
lines.append(f"- {status}{count}")
return {
ReportResponseKey.TITLE: ReportTitle.ATTENDANCE_SUMMARY,
ReportResponseKey.REPORT_TYPE: ReportPushType.ATTENDANCE_SUMMARY,
ReportResponseKey.WORK_DATE: target_date.isoformat(),
ReportResponseKey.TOTAL: total,
ReportResponseKey.ABNORMAL_TOTAL: abnormal_total,
ReportResponseKey.STATUS_COUNTS: status_counts,
ReportResponseKey.LINES: lines,
ReportResponseKey.CONTENT: "\n".join(lines),
}