```
feat(scheduler): 新增多种报表推送功能并重构调度器代码 - 新增考勤汇总、风险进度、工作日报、工作周报等报表推送功能 - 重构调度器中的报表推送逻辑,提取通用的 deliver_report 函数 - 添加新的定时任务配置项用于各种报表推送 - 实现飞书推送配置检查函数 _feishu_delivery_configured - 将重复的报表推送代码抽取为可复用的函数模式 ```
This commit is contained in:
@@ -22,21 +22,18 @@ from app.modules.feishu.constants import (
|
||||
FeishuPayloadKey,
|
||||
FeishuReplyType,
|
||||
)
|
||||
from app.modules.reports.constants import ReportResponseKey
|
||||
from app.modules.reports.chart import lifecycle_chart_alt, render_lifecycle_chart
|
||||
from app.modules.feishu.service import FeishuService
|
||||
from app.modules.reports.services import ReportService
|
||||
from app.modules.market.chart import render_market_chart
|
||||
from app.modules.market.service import MarketService
|
||||
from app.modules.risk.constants import RiskSummaryKey
|
||||
from app.modules.risk.services import RiskService
|
||||
from app.modules.reports.chart import lifecycle_chart_alt, render_lifecycle_chart
|
||||
from app.modules.reports.constants import ReportResponseKey
|
||||
from app.modules.reports.services import ReportService
|
||||
|
||||
DAILY_REPORT_KEYWORDS = ("日报", "晨报", "经营日报", "经营晨报")
|
||||
PROJECT_WEEKLY_KEYWORDS = ("周报", "项目周报")
|
||||
ATTENDANCE_KEYWORDS = ("打卡", "考勤", "attendance")
|
||||
RISK_KEYWORDS = ("风险", "预警", "risk")
|
||||
AI_COMMAND_PREFIXES = ("问 ", "ai ", "AI ", "/ask ")
|
||||
RISK_TITLE = "风险预警"
|
||||
DEFAULT_AI_PROMPT = "请说明你能做什么。"
|
||||
RULE_TITLE = "AI 学习规则"
|
||||
RULE_CREATE_PATTERN = re.compile(r"^学习规则(?:\s+(\d{1,3}))?\s*[::]\s*(.*)$")
|
||||
@@ -246,31 +243,21 @@ class FeishuCommandService:
|
||||
)
|
||||
|
||||
if any(keyword in command_text for keyword in RISK_KEYWORDS):
|
||||
summary = RiskService(self.db).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])}",
|
||||
]
|
||||
report = ReportService(self.db).risk_progress()
|
||||
if auto_reply:
|
||||
provider_response = self._send_card_if_configured(
|
||||
chat_id,
|
||||
RISK_TITLE,
|
||||
lines,
|
||||
report[ReportResponseKey.TITLE],
|
||||
report[ReportResponseKey.LINES],
|
||||
actor,
|
||||
)
|
||||
return _command_result(
|
||||
FeishuCommandName.RISK_SUMMARY,
|
||||
FeishuReplyType.CARD,
|
||||
RISK_TITLE,
|
||||
"\n".join(lines),
|
||||
report[ReportResponseKey.TITLE],
|
||||
report[ReportResponseKey.CONTENT],
|
||||
provider_response,
|
||||
lines,
|
||||
report[ReportResponseKey.LINES],
|
||||
)
|
||||
|
||||
prompt = command_text
|
||||
|
||||
@@ -6,10 +6,20 @@ class ReportType(StrEnum):
|
||||
WEEKLY = "weekly"
|
||||
|
||||
|
||||
class ReportPushType(StrEnum):
|
||||
DAILY_BRIEF = "daily_brief"
|
||||
PROJECT_WEEKLY = "project_weekly"
|
||||
ATTENDANCE_SUMMARY = "attendance_summary"
|
||||
RISK_PROGRESS = "risk_progress"
|
||||
WORK_DAILY = "work_daily"
|
||||
WORK_WEEKLY = "work_weekly"
|
||||
|
||||
|
||||
class ReportTitle(StrEnum):
|
||||
DAILY_BRIEF = "每日经营晨报"
|
||||
PROJECT_WEEKLY = "项目周报"
|
||||
ATTENDANCE_SUMMARY = "打卡汇总"
|
||||
RISK_PROGRESS = "项目风险进度"
|
||||
WORK_DAILY = "经营日报"
|
||||
WORK_WEEKLY = "经营周报"
|
||||
PROJECT_LIFECYCLE = "项目全生命周期报告"
|
||||
|
||||
@@ -4,9 +4,13 @@ from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.background.task_queue import (
|
||||
enqueue_attendance_summary_push,
|
||||
enqueue_daily_brief_push,
|
||||
enqueue_lifecycle_report,
|
||||
enqueue_project_weekly_push,
|
||||
enqueue_risk_progress_push,
|
||||
enqueue_work_daily_push,
|
||||
enqueue_work_weekly_push,
|
||||
)
|
||||
from app.core.database import get_db
|
||||
from app.core.security import ApiPrincipal, require_api_key, require_operations_enabled
|
||||
@@ -36,6 +40,13 @@ def project_weekly(
|
||||
return ReportService(db).project_weekly()
|
||||
|
||||
|
||||
@router.get("/risk-progress")
|
||||
def risk_progress(
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
return ReportService(db).risk_progress()
|
||||
|
||||
|
||||
@router.get("/project-lifecycle")
|
||||
def project_lifecycle_report(
|
||||
project_code: str | None = None,
|
||||
@@ -175,8 +186,9 @@ def push_daily_brief(
|
||||
db: Session = Depends(get_db),
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
report = ReportService(db).daily_brief()
|
||||
return ReportService(db).push_report(
|
||||
service = ReportService(db)
|
||||
report = service.daily_brief()
|
||||
return service.push_report(
|
||||
report,
|
||||
payload.receive_id,
|
||||
payload.receive_id_type,
|
||||
@@ -190,8 +202,73 @@ def push_project_weekly(
|
||||
db: Session = Depends(get_db),
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
report = ReportService(db).project_weekly()
|
||||
return ReportService(db).push_report(
|
||||
service = ReportService(db)
|
||||
report = service.project_weekly()
|
||||
return service.push_report(
|
||||
report,
|
||||
payload.receive_id,
|
||||
payload.receive_id_type,
|
||||
principal.actor,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/attendance-summary/push")
|
||||
def push_attendance_summary(
|
||||
payload: PushReportRequest,
|
||||
db: Session = Depends(get_db),
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
service = ReportService(db)
|
||||
report = service.attendance_summary()
|
||||
return service.push_report(
|
||||
report,
|
||||
payload.receive_id,
|
||||
payload.receive_id_type,
|
||||
principal.actor,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/risk-progress/push")
|
||||
def push_risk_progress(
|
||||
payload: PushReportRequest,
|
||||
db: Session = Depends(get_db),
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
service = ReportService(db)
|
||||
report = service.risk_progress()
|
||||
return service.push_report(
|
||||
report,
|
||||
payload.receive_id,
|
||||
payload.receive_id_type,
|
||||
principal.actor,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/work-daily/push")
|
||||
def push_work_daily(
|
||||
payload: PushReportRequest,
|
||||
db: Session = Depends(get_db),
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
service = ReportService(db)
|
||||
report = service.work_daily_report(reporter=principal.actor, actor=principal.actor)
|
||||
return service.push_report(
|
||||
report,
|
||||
payload.receive_id,
|
||||
payload.receive_id_type,
|
||||
principal.actor,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/work-weekly/push")
|
||||
def push_work_weekly(
|
||||
payload: PushReportRequest,
|
||||
db: Session = Depends(get_db),
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
service = ReportService(db)
|
||||
report = service.work_weekly_report(reporter=principal.actor, actor=principal.actor)
|
||||
return service.push_report(
|
||||
report,
|
||||
payload.receive_id,
|
||||
payload.receive_id_type,
|
||||
@@ -221,3 +298,51 @@ def enqueue_project_weekly(
|
||||
receive_id_type=payload.receive_id_type,
|
||||
actor=principal.actor,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/attendance-summary/enqueue")
|
||||
def enqueue_attendance_summary(
|
||||
payload: PushReportRequest,
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
return enqueue_attendance_summary_push(
|
||||
receive_id=payload.receive_id,
|
||||
receive_id_type=payload.receive_id_type,
|
||||
actor=principal.actor,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/risk-progress/enqueue")
|
||||
def enqueue_risk_progress(
|
||||
payload: PushReportRequest,
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
return enqueue_risk_progress_push(
|
||||
receive_id=payload.receive_id,
|
||||
receive_id_type=payload.receive_id_type,
|
||||
actor=principal.actor,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/work-daily/enqueue")
|
||||
def enqueue_work_daily(
|
||||
payload: PushReportRequest,
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
return enqueue_work_daily_push(
|
||||
receive_id=payload.receive_id,
|
||||
receive_id_type=payload.receive_id_type,
|
||||
actor=principal.actor,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/work-weekly/enqueue")
|
||||
def enqueue_work_weekly(
|
||||
payload: PushReportRequest,
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
return enqueue_work_weekly_push(
|
||||
receive_id=payload.receive_id,
|
||||
receive_id_type=payload.receive_id_type,
|
||||
actor=principal.actor,
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ from app.modules.business.models import (
|
||||
WorkTask,
|
||||
)
|
||||
from app.modules.reports.constants import (
|
||||
ReportPushType,
|
||||
ReportResponseKey,
|
||||
ReportTitle,
|
||||
)
|
||||
@@ -65,6 +66,7 @@ class ReportSummaryMixin:
|
||||
]
|
||||
return {
|
||||
ReportResponseKey.TITLE: ReportTitle.DAILY_BRIEF,
|
||||
ReportResponseKey.REPORT_TYPE: ReportPushType.DAILY_BRIEF,
|
||||
ReportResponseKey.LINES: lines,
|
||||
ReportResponseKey.CONTENT: "\n".join(lines),
|
||||
}
|
||||
@@ -93,6 +95,28 @@ class ReportSummaryMixin:
|
||||
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),
|
||||
}
|
||||
@@ -122,6 +146,7 @@ class ReportSummaryMixin:
|
||||
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,
|
||||
|
||||
@@ -30,6 +30,7 @@ from app.modules.events.constants import (
|
||||
)
|
||||
from app.modules.events.services import EventService
|
||||
from app.modules.reports.constants import (
|
||||
ReportPushType,
|
||||
ReportResponseKey,
|
||||
ReportTitle,
|
||||
ReportType,
|
||||
@@ -41,6 +42,34 @@ from app.modules.reports.services.common import _json_safe, _next_code
|
||||
|
||||
|
||||
class ReportWorkReportMixin:
|
||||
def work_daily_report(
|
||||
self,
|
||||
reporter: str = ActorValue.SCHEDULER,
|
||||
actor: str = ActorValue.SCHEDULER,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a transient work daily report for Feishu delivery."""
|
||||
|
||||
return self._transient_work_report(
|
||||
report_type=ReportType.DAILY,
|
||||
push_type=ReportPushType.WORK_DAILY,
|
||||
reporter=reporter,
|
||||
actor=actor,
|
||||
)
|
||||
|
||||
def work_weekly_report(
|
||||
self,
|
||||
reporter: str = ActorValue.SCHEDULER,
|
||||
actor: str = ActorValue.SCHEDULER,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a transient work weekly report for Feishu delivery."""
|
||||
|
||||
return self._transient_work_report(
|
||||
report_type=ReportType.WEEKLY,
|
||||
push_type=ReportPushType.WORK_WEEKLY,
|
||||
reporter=reporter,
|
||||
actor=actor,
|
||||
)
|
||||
|
||||
def generate_work_report(
|
||||
self,
|
||||
report_type: str = ReportType.DAILY,
|
||||
@@ -119,6 +148,23 @@ class ReportWorkReportMixin:
|
||||
|
||||
return {ReportResponseKey.REPORT: report, ReportResponseKey.DATA: record_data}
|
||||
|
||||
def _transient_work_report(
|
||||
self,
|
||||
report_type: str,
|
||||
push_type: str,
|
||||
reporter: str,
|
||||
actor: str,
|
||||
) -> dict[str, Any]:
|
||||
result = self.generate_work_report(
|
||||
report_type=report_type,
|
||||
reporter=reporter,
|
||||
persist=False,
|
||||
actor=actor,
|
||||
)
|
||||
report = dict(result[ReportResponseKey.REPORT])
|
||||
report[ReportResponseKey.REPORT_TYPE] = push_type
|
||||
return report
|
||||
|
||||
def _resolve_period(
|
||||
self,
|
||||
report_type: str,
|
||||
|
||||
Reference in New Issue
Block a user