```
feat(scheduler): 新增多种报表推送功能并重构调度器代码 - 新增考勤汇总、风险进度、工作日报、工作周报等报表推送功能 - 重构调度器中的报表推送逻辑,提取通用的 deliver_report 函数 - 添加新的定时任务配置项用于各种报表推送 - 实现飞书推送配置检查函数 _feishu_delivery_configured - 将重复的报表推送代码抽取为可复用的函数模式 ```
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from socket import gethostname
|
from socket import gethostname
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -35,6 +36,7 @@ def create_scheduler(app: FastAPI | None = None) -> Any:
|
|||||||
|
|
||||||
from app.core.database import SessionLocal
|
from app.core.database import SessionLocal
|
||||||
from app.core.background.task_queue import (
|
from app.core.background.task_queue import (
|
||||||
|
enqueue_attendance_summary_push,
|
||||||
enqueue_daily_brief_push,
|
enqueue_daily_brief_push,
|
||||||
enqueue_event_dispatch,
|
enqueue_event_dispatch,
|
||||||
enqueue_legacy_project_sync,
|
enqueue_legacy_project_sync,
|
||||||
@@ -42,6 +44,9 @@ def create_scheduler(app: FastAPI | None = None) -> Any:
|
|||||||
enqueue_lifecycle_report,
|
enqueue_lifecycle_report,
|
||||||
enqueue_market_report,
|
enqueue_market_report,
|
||||||
enqueue_project_weekly_push,
|
enqueue_project_weekly_push,
|
||||||
|
enqueue_risk_progress_push,
|
||||||
|
enqueue_work_daily_push,
|
||||||
|
enqueue_work_weekly_push,
|
||||||
)
|
)
|
||||||
from app.modules.observability.service import ObservabilityService
|
from app.modules.observability.service import ObservabilityService
|
||||||
from app.modules.reports.services import ReportService
|
from app.modules.reports.services import ReportService
|
||||||
@@ -49,25 +54,27 @@ def create_scheduler(app: FastAPI | None = None) -> Any:
|
|||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
scheduler = BackgroundScheduler(timezone="Asia/Shanghai")
|
scheduler = BackgroundScheduler(timezone="Asia/Shanghai")
|
||||||
|
|
||||||
def run_daily_brief() -> None:
|
def deliver_report(
|
||||||
|
state_key: str,
|
||||||
|
build_report: Callable[[Any], dict[str, Any]],
|
||||||
|
enqueue_push: Callable[..., dict[str, Any]],
|
||||||
|
) -> None:
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
report = ReportService(db).daily_brief()
|
service = ReportService(db)
|
||||||
_set_state(app, "last_daily_brief", report)
|
report = build_report(service)
|
||||||
if (
|
_set_state(app, state_key, report)
|
||||||
settings.feishu_app_id
|
if not _feishu_delivery_configured(settings):
|
||||||
and settings.feishu_app_secret
|
return
|
||||||
and settings.feishu_default_chat_id
|
|
||||||
):
|
|
||||||
if settings.task_queue_enabled:
|
if settings.task_queue_enabled:
|
||||||
dispatch = enqueue_daily_brief_push(
|
dispatch = enqueue_push(
|
||||||
receive_id=settings.feishu_default_chat_id,
|
receive_id=settings.feishu_default_chat_id,
|
||||||
receive_id_type=FeishuReceiveIdType.CHAT_ID,
|
receive_id_type=FeishuReceiveIdType.CHAT_ID,
|
||||||
actor=ActorValue.SCHEDULER,
|
actor=ActorValue.SCHEDULER,
|
||||||
)
|
)
|
||||||
_set_state(app, "last_daily_brief_dispatch", dispatch)
|
_set_state(app, f"{state_key}_dispatch", dispatch)
|
||||||
return
|
return
|
||||||
ReportService(db).push_report(
|
service.push_report(
|
||||||
report,
|
report,
|
||||||
receive_id=settings.feishu_default_chat_id,
|
receive_id=settings.feishu_default_chat_id,
|
||||||
receive_id_type=FeishuReceiveIdType.CHAT_ID,
|
receive_id_type=FeishuReceiveIdType.CHAT_ID,
|
||||||
@@ -76,32 +83,53 @@ def create_scheduler(app: FastAPI | None = None) -> Any:
|
|||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
def run_daily_brief() -> None:
|
||||||
|
deliver_report(
|
||||||
|
"last_daily_brief",
|
||||||
|
lambda service: service.daily_brief(),
|
||||||
|
enqueue_daily_brief_push,
|
||||||
|
)
|
||||||
|
|
||||||
def run_project_weekly() -> None:
|
def run_project_weekly() -> None:
|
||||||
db = SessionLocal()
|
deliver_report(
|
||||||
try:
|
"last_project_weekly",
|
||||||
report = ReportService(db).project_weekly()
|
lambda service: service.project_weekly(),
|
||||||
_set_state(app, "last_project_weekly", report)
|
enqueue_project_weekly_push,
|
||||||
if (
|
|
||||||
settings.feishu_app_id
|
|
||||||
and settings.feishu_app_secret
|
|
||||||
and settings.feishu_default_chat_id
|
|
||||||
):
|
|
||||||
if settings.task_queue_enabled:
|
|
||||||
dispatch = enqueue_project_weekly_push(
|
|
||||||
receive_id=settings.feishu_default_chat_id,
|
|
||||||
receive_id_type=FeishuReceiveIdType.CHAT_ID,
|
|
||||||
actor=ActorValue.SCHEDULER,
|
|
||||||
)
|
)
|
||||||
_set_state(app, "last_project_weekly_dispatch", dispatch)
|
|
||||||
return
|
def run_attendance_summary() -> None:
|
||||||
ReportService(db).push_report(
|
deliver_report(
|
||||||
report,
|
"last_attendance_summary",
|
||||||
receive_id=settings.feishu_default_chat_id,
|
lambda service: service.attendance_summary(),
|
||||||
receive_id_type=FeishuReceiveIdType.CHAT_ID,
|
enqueue_attendance_summary_push,
|
||||||
actor=ActorValue.SCHEDULER,
|
)
|
||||||
|
|
||||||
|
def run_risk_progress() -> None:
|
||||||
|
deliver_report(
|
||||||
|
"last_risk_progress",
|
||||||
|
lambda service: service.risk_progress(),
|
||||||
|
enqueue_risk_progress_push,
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_work_daily() -> None:
|
||||||
|
deliver_report(
|
||||||
|
"last_work_daily",
|
||||||
|
lambda service: service.work_daily_report(
|
||||||
|
reporter=ActorValue.SCHEDULER,
|
||||||
|
actor=ActorValue.SCHEDULER,
|
||||||
|
),
|
||||||
|
enqueue_work_daily_push,
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_work_weekly() -> None:
|
||||||
|
deliver_report(
|
||||||
|
"last_work_weekly",
|
||||||
|
lambda service: service.work_weekly_report(
|
||||||
|
reporter=ActorValue.SCHEDULER,
|
||||||
|
actor=ActorValue.SCHEDULER,
|
||||||
|
),
|
||||||
|
enqueue_work_weekly_push,
|
||||||
)
|
)
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
def run_legacy_project_sync() -> None:
|
def run_legacy_project_sync() -> None:
|
||||||
dispatch = enqueue_legacy_project_sync(actor=ActorValue.SCHEDULER)
|
dispatch = enqueue_legacy_project_sync(actor=ActorValue.SCHEDULER)
|
||||||
@@ -196,6 +224,39 @@ def create_scheduler(app: FastAPI | None = None) -> Any:
|
|||||||
id="project_weekly_push",
|
id="project_weekly_push",
|
||||||
replace_existing=True,
|
replace_existing=True,
|
||||||
)
|
)
|
||||||
|
scheduler.add_job(
|
||||||
|
run_attendance_summary,
|
||||||
|
trigger="cron",
|
||||||
|
hour=settings.attendance_summary_cron_hour,
|
||||||
|
minute=settings.attendance_summary_cron_minute,
|
||||||
|
id="attendance_summary_push",
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
|
scheduler.add_job(
|
||||||
|
run_risk_progress,
|
||||||
|
trigger="cron",
|
||||||
|
hour=settings.risk_progress_cron_hour,
|
||||||
|
minute=settings.risk_progress_cron_minute,
|
||||||
|
id="risk_progress_push",
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
|
scheduler.add_job(
|
||||||
|
run_work_daily,
|
||||||
|
trigger="cron",
|
||||||
|
hour=settings.work_daily_cron_hour,
|
||||||
|
minute=settings.work_daily_cron_minute,
|
||||||
|
id="work_daily_push",
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
|
scheduler.add_job(
|
||||||
|
run_work_weekly,
|
||||||
|
trigger="cron",
|
||||||
|
day_of_week=settings.work_weekly_report_day_of_week,
|
||||||
|
hour=settings.work_weekly_cron_hour,
|
||||||
|
minute=settings.work_weekly_cron_minute,
|
||||||
|
id="work_weekly_push",
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
if settings.event_dispatch_enabled:
|
if settings.event_dispatch_enabled:
|
||||||
scheduler.add_job(
|
scheduler.add_job(
|
||||||
run_event_dispatch,
|
run_event_dispatch,
|
||||||
@@ -271,3 +332,11 @@ def create_scheduler(app: FastAPI | None = None) -> Any:
|
|||||||
def _set_state(app: FastAPI | None, key: str, value: Any) -> None:
|
def _set_state(app: FastAPI | None, key: str, value: Any) -> None:
|
||||||
if app is not None:
|
if app is not None:
|
||||||
setattr(app.state, key, value)
|
setattr(app.state, key, value)
|
||||||
|
|
||||||
|
|
||||||
|
def _feishu_delivery_configured(settings: Any) -> bool:
|
||||||
|
return bool(
|
||||||
|
settings.feishu_app_id
|
||||||
|
and settings.feishu_app_secret
|
||||||
|
and settings.feishu_default_chat_id
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
from app.core.background.task_queue.constants import (
|
from app.core.background.task_queue.constants import (
|
||||||
TASK_DISPATCH_PENDING_EVENTS,
|
TASK_DISPATCH_PENDING_EVENTS,
|
||||||
TASK_GENERATE_RISK_EVENTS,
|
TASK_GENERATE_RISK_EVENTS,
|
||||||
|
TASK_PUSH_ATTENDANCE_SUMMARY,
|
||||||
TASK_PUSH_DAILY_BRIEF,
|
TASK_PUSH_DAILY_BRIEF,
|
||||||
TASK_PUSH_PROJECT_WEEKLY,
|
TASK_PUSH_PROJECT_WEEKLY,
|
||||||
|
TASK_PUSH_RISK_PROGRESS,
|
||||||
|
TASK_PUSH_WORK_DAILY,
|
||||||
|
TASK_PUSH_WORK_WEEKLY,
|
||||||
TASK_SYNC_LEGACY_PROJECTS,
|
TASK_SYNC_LEGACY_PROJECTS,
|
||||||
TASK_SYNC_LEGACY_TASKS,
|
TASK_SYNC_LEGACY_TASKS,
|
||||||
TASK_RUN_LIFECYCLE,
|
TASK_RUN_LIFECYCLE,
|
||||||
@@ -18,8 +22,12 @@ from app.core.background.task_queue.legacy import (
|
|||||||
from app.core.background.task_queue.lifecycle import enqueue_lifecycle_report
|
from app.core.background.task_queue.lifecycle import enqueue_lifecycle_report
|
||||||
from app.core.background.task_queue.market import enqueue_market_close, enqueue_market_report
|
from app.core.background.task_queue.market import enqueue_market_close, enqueue_market_report
|
||||||
from app.core.background.task_queue.reports import (
|
from app.core.background.task_queue.reports import (
|
||||||
|
enqueue_attendance_summary_push,
|
||||||
enqueue_daily_brief_push,
|
enqueue_daily_brief_push,
|
||||||
enqueue_project_weekly_push,
|
enqueue_project_weekly_push,
|
||||||
|
enqueue_risk_progress_push,
|
||||||
|
enqueue_work_daily_push,
|
||||||
|
enqueue_work_weekly_push,
|
||||||
)
|
)
|
||||||
from app.core.background.task_queue.risk import enqueue_risk_event_generation
|
from app.core.background.task_queue.risk import enqueue_risk_event_generation
|
||||||
|
|
||||||
@@ -27,14 +35,19 @@ from app.core.background.task_queue.risk import enqueue_risk_event_generation
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"TASK_DISPATCH_PENDING_EVENTS",
|
"TASK_DISPATCH_PENDING_EVENTS",
|
||||||
"TASK_GENERATE_RISK_EVENTS",
|
"TASK_GENERATE_RISK_EVENTS",
|
||||||
|
"TASK_PUSH_ATTENDANCE_SUMMARY",
|
||||||
"TASK_PUSH_DAILY_BRIEF",
|
"TASK_PUSH_DAILY_BRIEF",
|
||||||
"TASK_PUSH_PROJECT_WEEKLY",
|
"TASK_PUSH_PROJECT_WEEKLY",
|
||||||
|
"TASK_PUSH_RISK_PROGRESS",
|
||||||
|
"TASK_PUSH_WORK_DAILY",
|
||||||
|
"TASK_PUSH_WORK_WEEKLY",
|
||||||
"TASK_SYNC_LEGACY_PROJECTS",
|
"TASK_SYNC_LEGACY_PROJECTS",
|
||||||
"TASK_SYNC_LEGACY_TASKS",
|
"TASK_SYNC_LEGACY_TASKS",
|
||||||
"TASK_RUN_LIFECYCLE",
|
"TASK_RUN_LIFECYCLE",
|
||||||
"TASK_RUN_MARKET_CLOSE",
|
"TASK_RUN_MARKET_CLOSE",
|
||||||
"TASK_RUN_MARKET_REPORT",
|
"TASK_RUN_MARKET_REPORT",
|
||||||
"dispatch_task",
|
"dispatch_task",
|
||||||
|
"enqueue_attendance_summary_push",
|
||||||
"enqueue_daily_brief_push",
|
"enqueue_daily_brief_push",
|
||||||
"enqueue_event_dispatch",
|
"enqueue_event_dispatch",
|
||||||
"enqueue_legacy_project_sync",
|
"enqueue_legacy_project_sync",
|
||||||
@@ -43,5 +56,8 @@ __all__ = [
|
|||||||
"enqueue_market_close",
|
"enqueue_market_close",
|
||||||
"enqueue_market_report",
|
"enqueue_market_report",
|
||||||
"enqueue_project_weekly_push",
|
"enqueue_project_weekly_push",
|
||||||
|
"enqueue_risk_progress_push",
|
||||||
"enqueue_risk_event_generation",
|
"enqueue_risk_event_generation",
|
||||||
|
"enqueue_work_daily_push",
|
||||||
|
"enqueue_work_weekly_push",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
TASK_PUSH_DAILY_BRIEF = "reports.push_daily_brief"
|
TASK_PUSH_DAILY_BRIEF = "reports.push_daily_brief"
|
||||||
TASK_PUSH_PROJECT_WEEKLY = "reports.push_project_weekly"
|
TASK_PUSH_PROJECT_WEEKLY = "reports.push_project_weekly"
|
||||||
|
TASK_PUSH_ATTENDANCE_SUMMARY = "reports.push_attendance_summary"
|
||||||
|
TASK_PUSH_RISK_PROGRESS = "reports.push_risk_progress"
|
||||||
|
TASK_PUSH_WORK_DAILY = "reports.push_work_daily"
|
||||||
|
TASK_PUSH_WORK_WEEKLY = "reports.push_work_weekly"
|
||||||
TASK_GENERATE_RISK_EVENTS = "risks.generate_events"
|
TASK_GENERATE_RISK_EVENTS = "risks.generate_events"
|
||||||
TASK_SYNC_LEGACY_PROJECTS = "legacy.sync_projects"
|
TASK_SYNC_LEGACY_PROJECTS = "legacy.sync_projects"
|
||||||
TASK_SYNC_LEGACY_TASKS = "legacy.sync_tasks"
|
TASK_SYNC_LEGACY_TASKS = "legacy.sync_tasks"
|
||||||
|
|||||||
@@ -1,106 +1,187 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from app.core.config import get_settings
|
|
||||||
from app.modules.feishu.constants import FeishuReceiveIdType
|
|
||||||
from app.core.background.task_queue.constants import (
|
from app.core.background.task_queue.constants import (
|
||||||
|
TASK_PUSH_ATTENDANCE_SUMMARY,
|
||||||
TASK_PUSH_DAILY_BRIEF,
|
TASK_PUSH_DAILY_BRIEF,
|
||||||
TASK_PUSH_PROJECT_WEEKLY,
|
TASK_PUSH_PROJECT_WEEKLY,
|
||||||
|
TASK_PUSH_RISK_PROGRESS,
|
||||||
|
TASK_PUSH_WORK_DAILY,
|
||||||
|
TASK_PUSH_WORK_WEEKLY,
|
||||||
)
|
)
|
||||||
from app.core.background.task_queue.dispatcher import dispatch_task
|
from app.core.background.task_queue.dispatcher import dispatch_task
|
||||||
|
from app.core.config import get_settings
|
||||||
|
from app.core.constants import ActorValue
|
||||||
|
from app.modules.feishu.constants import FeishuReceiveIdType
|
||||||
|
from app.modules.reports.constants import ReportPushType, ReportTitle
|
||||||
|
|
||||||
|
ReportBuilder = Callable[[Any, str], dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
def enqueue_daily_brief_push(
|
def enqueue_daily_brief_push(
|
||||||
receive_id: str | None = None,
|
receive_id: str | None = None,
|
||||||
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
||||||
actor: str = "scheduler",
|
actor: str = ActorValue.SCHEDULER,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
settings = get_settings()
|
return _enqueue_report_push(
|
||||||
if settings.task_queue_enabled:
|
task_name=TASK_PUSH_DAILY_BRIEF,
|
||||||
from app.core.database import SessionLocal
|
report_type=ReportPushType.DAILY_BRIEF,
|
||||||
from app.modules.reports.constants import ReportPushStatus, ReportTitle, ReportType
|
|
||||||
from app.modules.reports.services import ReportService
|
|
||||||
from app.tasks import celery_app
|
|
||||||
|
|
||||||
db = SessionLocal()
|
|
||||||
try:
|
|
||||||
push_run = ReportService(db).create_push_run(
|
|
||||||
report_type=ReportType.DAILY,
|
|
||||||
title=ReportTitle.DAILY_BRIEF,
|
title=ReportTitle.DAILY_BRIEF,
|
||||||
|
build_report=_build_daily_brief,
|
||||||
receive_id=receive_id,
|
receive_id=receive_id,
|
||||||
receive_id_type=receive_id_type,
|
receive_id_type=receive_id_type,
|
||||||
actor=actor,
|
actor=actor,
|
||||||
status=ReportPushStatus.QUEUED,
|
|
||||||
)
|
|
||||||
async_result = celery_app.signature(
|
|
||||||
TASK_PUSH_DAILY_BRIEF,
|
|
||||||
kwargs={
|
|
||||||
"receive_id": receive_id,
|
|
||||||
"receive_id_type": receive_id_type,
|
|
||||||
"actor": actor,
|
|
||||||
"push_run_code": push_run.code,
|
|
||||||
},
|
|
||||||
).apply_async()
|
|
||||||
ReportService(db).update_push_run(
|
|
||||||
push_run.code,
|
|
||||||
ReportPushStatus.QUEUED,
|
|
||||||
task_id=async_result.id,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
return {
|
|
||||||
"queued": True,
|
|
||||||
"mode": "celery",
|
|
||||||
"task_name": TASK_PUSH_DAILY_BRIEF,
|
|
||||||
"task_id": async_result.id,
|
|
||||||
"push_run_code": push_run.code,
|
|
||||||
}
|
|
||||||
|
|
||||||
def inline() -> Any:
|
|
||||||
from app.core.database import SessionLocal
|
|
||||||
from app.modules.reports.services import ReportService
|
|
||||||
|
|
||||||
db = SessionLocal()
|
|
||||||
try:
|
|
||||||
report = ReportService(db).daily_brief()
|
|
||||||
return ReportService(db).push_report(report, receive_id, receive_id_type, actor)
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
return dispatch_task(
|
|
||||||
TASK_PUSH_DAILY_BRIEF,
|
|
||||||
{
|
|
||||||
"receive_id": receive_id,
|
|
||||||
"receive_id_type": receive_id_type,
|
|
||||||
"actor": actor,
|
|
||||||
},
|
|
||||||
inline,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def enqueue_project_weekly_push(
|
def enqueue_project_weekly_push(
|
||||||
receive_id: str | None = None,
|
receive_id: str | None = None,
|
||||||
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
||||||
actor: str = "scheduler",
|
actor: str = ActorValue.SCHEDULER,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return _enqueue_report_push(
|
||||||
|
task_name=TASK_PUSH_PROJECT_WEEKLY,
|
||||||
|
report_type=ReportPushType.PROJECT_WEEKLY,
|
||||||
|
title=ReportTitle.PROJECT_WEEKLY,
|
||||||
|
build_report=_build_project_weekly,
|
||||||
|
receive_id=receive_id,
|
||||||
|
receive_id_type=receive_id_type,
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def enqueue_attendance_summary_push(
|
||||||
|
receive_id: str | None = None,
|
||||||
|
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
||||||
|
actor: str = ActorValue.SCHEDULER,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return _enqueue_report_push(
|
||||||
|
task_name=TASK_PUSH_ATTENDANCE_SUMMARY,
|
||||||
|
report_type=ReportPushType.ATTENDANCE_SUMMARY,
|
||||||
|
title=ReportTitle.ATTENDANCE_SUMMARY,
|
||||||
|
build_report=_build_attendance_summary,
|
||||||
|
receive_id=receive_id,
|
||||||
|
receive_id_type=receive_id_type,
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def enqueue_risk_progress_push(
|
||||||
|
receive_id: str | None = None,
|
||||||
|
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
||||||
|
actor: str = ActorValue.SCHEDULER,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return _enqueue_report_push(
|
||||||
|
task_name=TASK_PUSH_RISK_PROGRESS,
|
||||||
|
report_type=ReportPushType.RISK_PROGRESS,
|
||||||
|
title=ReportTitle.RISK_PROGRESS,
|
||||||
|
build_report=_build_risk_progress,
|
||||||
|
receive_id=receive_id,
|
||||||
|
receive_id_type=receive_id_type,
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def enqueue_work_daily_push(
|
||||||
|
receive_id: str | None = None,
|
||||||
|
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
||||||
|
actor: str = ActorValue.SCHEDULER,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return _enqueue_report_push(
|
||||||
|
task_name=TASK_PUSH_WORK_DAILY,
|
||||||
|
report_type=ReportPushType.WORK_DAILY,
|
||||||
|
title=ReportTitle.WORK_DAILY,
|
||||||
|
build_report=_build_work_daily,
|
||||||
|
receive_id=receive_id,
|
||||||
|
receive_id_type=receive_id_type,
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def enqueue_work_weekly_push(
|
||||||
|
receive_id: str | None = None,
|
||||||
|
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
||||||
|
actor: str = ActorValue.SCHEDULER,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return _enqueue_report_push(
|
||||||
|
task_name=TASK_PUSH_WORK_WEEKLY,
|
||||||
|
report_type=ReportPushType.WORK_WEEKLY,
|
||||||
|
title=ReportTitle.WORK_WEEKLY,
|
||||||
|
build_report=_build_work_weekly,
|
||||||
|
receive_id=receive_id,
|
||||||
|
receive_id_type=receive_id_type,
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _enqueue_report_push(
|
||||||
|
task_name: str,
|
||||||
|
report_type: str,
|
||||||
|
title: str,
|
||||||
|
build_report: ReportBuilder,
|
||||||
|
receive_id: str | None,
|
||||||
|
receive_id_type: str,
|
||||||
|
actor: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
if settings.task_queue_enabled:
|
if settings.task_queue_enabled:
|
||||||
|
return _queue_celery_report_push(
|
||||||
|
task_name=task_name,
|
||||||
|
report_type=report_type,
|
||||||
|
title=title,
|
||||||
|
receive_id=receive_id,
|
||||||
|
receive_id_type=receive_id_type,
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
|
||||||
|
def inline() -> Any:
|
||||||
from app.core.database import SessionLocal
|
from app.core.database import SessionLocal
|
||||||
from app.modules.reports.constants import ReportPushStatus, ReportTitle, ReportType
|
from app.modules.reports.services import ReportService
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
service = ReportService(db)
|
||||||
|
report = build_report(service, actor)
|
||||||
|
return service.push_report(report, receive_id, receive_id_type, actor)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
return dispatch_task(
|
||||||
|
task_name,
|
||||||
|
{
|
||||||
|
"receive_id": receive_id,
|
||||||
|
"receive_id_type": receive_id_type,
|
||||||
|
"actor": actor,
|
||||||
|
},
|
||||||
|
inline,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _queue_celery_report_push(
|
||||||
|
task_name: str,
|
||||||
|
report_type: str,
|
||||||
|
title: str,
|
||||||
|
receive_id: str | None,
|
||||||
|
receive_id_type: str,
|
||||||
|
actor: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
from app.core.database import SessionLocal
|
||||||
|
from app.modules.reports.constants import ReportPushStatus
|
||||||
from app.modules.reports.services import ReportService
|
from app.modules.reports.services import ReportService
|
||||||
from app.tasks import celery_app
|
from app.tasks import celery_app
|
||||||
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
push_run = ReportService(db).create_push_run(
|
push_run = ReportService(db).create_push_run(
|
||||||
report_type=ReportType.WEEKLY,
|
report_type=report_type,
|
||||||
title=ReportTitle.PROJECT_WEEKLY,
|
title=title,
|
||||||
receive_id=receive_id,
|
receive_id=receive_id,
|
||||||
receive_id_type=receive_id_type,
|
receive_id_type=receive_id_type,
|
||||||
actor=actor,
|
actor=actor,
|
||||||
status=ReportPushStatus.QUEUED,
|
status=ReportPushStatus.QUEUED,
|
||||||
)
|
)
|
||||||
async_result = celery_app.signature(
|
async_result = celery_app.signature(
|
||||||
TASK_PUSH_PROJECT_WEEKLY,
|
task_name,
|
||||||
kwargs={
|
kwargs={
|
||||||
"receive_id": receive_id,
|
"receive_id": receive_id,
|
||||||
"receive_id_type": receive_id_type,
|
"receive_id_type": receive_id_type,
|
||||||
@@ -118,28 +199,35 @@ def enqueue_project_weekly_push(
|
|||||||
return {
|
return {
|
||||||
"queued": True,
|
"queued": True,
|
||||||
"mode": "celery",
|
"mode": "celery",
|
||||||
"task_name": TASK_PUSH_PROJECT_WEEKLY,
|
"task_name": task_name,
|
||||||
"task_id": async_result.id,
|
"task_id": async_result.id,
|
||||||
"push_run_code": push_run.code,
|
"push_run_code": push_run.code,
|
||||||
}
|
}
|
||||||
|
|
||||||
def inline() -> Any:
|
|
||||||
from app.core.database import SessionLocal
|
|
||||||
from app.modules.reports.services import ReportService
|
|
||||||
|
|
||||||
db = SessionLocal()
|
def _build_daily_brief(service: Any, actor: str) -> dict[str, Any]:
|
||||||
try:
|
_ = actor
|
||||||
report = ReportService(db).project_weekly()
|
return service.daily_brief()
|
||||||
return ReportService(db).push_report(report, receive_id, receive_id_type, actor)
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
return dispatch_task(
|
|
||||||
TASK_PUSH_PROJECT_WEEKLY,
|
def _build_project_weekly(service: Any, actor: str) -> dict[str, Any]:
|
||||||
{
|
_ = actor
|
||||||
"receive_id": receive_id,
|
return service.project_weekly()
|
||||||
"receive_id_type": receive_id_type,
|
|
||||||
"actor": actor,
|
|
||||||
},
|
def _build_attendance_summary(service: Any, actor: str) -> dict[str, Any]:
|
||||||
inline,
|
_ = actor
|
||||||
)
|
return service.attendance_summary()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_risk_progress(service: Any, actor: str) -> dict[str, Any]:
|
||||||
|
_ = actor
|
||||||
|
return service.risk_progress()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_work_daily(service: Any, actor: str) -> dict[str, Any]:
|
||||||
|
return service.work_daily_report(reporter=actor, actor=actor)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_work_weekly(service: Any, actor: str) -> dict[str, Any]:
|
||||||
|
return service.work_weekly_report(reporter=actor, actor=actor)
|
||||||
|
|||||||
@@ -86,6 +86,15 @@ class Settings(BaseSettings):
|
|||||||
celery_result_backend_url: str | None = None
|
celery_result_backend_url: str | None = None
|
||||||
daily_brief_cron_hour: int = 9
|
daily_brief_cron_hour: int = 9
|
||||||
daily_brief_cron_minute: int = 0
|
daily_brief_cron_minute: int = 0
|
||||||
|
attendance_summary_cron_hour: int = 18
|
||||||
|
attendance_summary_cron_minute: int = 0
|
||||||
|
risk_progress_cron_hour: int = 17
|
||||||
|
risk_progress_cron_minute: int = 30
|
||||||
|
work_daily_cron_hour: int = 18
|
||||||
|
work_daily_cron_minute: int = 30
|
||||||
|
work_weekly_report_day_of_week: str = "fri"
|
||||||
|
work_weekly_cron_hour: int = 18
|
||||||
|
work_weekly_cron_minute: int = 45
|
||||||
weekly_project_report_day_of_week: str = "mon"
|
weekly_project_report_day_of_week: str = "mon"
|
||||||
weekly_project_report_cron_hour: int = 9
|
weekly_project_report_cron_hour: int = 9
|
||||||
weekly_project_report_cron_minute: int = 30
|
weekly_project_report_cron_minute: int = 30
|
||||||
|
|||||||
@@ -22,21 +22,18 @@ from app.modules.feishu.constants import (
|
|||||||
FeishuPayloadKey,
|
FeishuPayloadKey,
|
||||||
FeishuReplyType,
|
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.feishu.service import FeishuService
|
||||||
from app.modules.reports.services import ReportService
|
|
||||||
from app.modules.market.chart import render_market_chart
|
from app.modules.market.chart import render_market_chart
|
||||||
from app.modules.market.service import MarketService
|
from app.modules.market.service import MarketService
|
||||||
from app.modules.risk.constants import RiskSummaryKey
|
from app.modules.reports.chart import lifecycle_chart_alt, render_lifecycle_chart
|
||||||
from app.modules.risk.services import RiskService
|
from app.modules.reports.constants import ReportResponseKey
|
||||||
|
from app.modules.reports.services import ReportService
|
||||||
|
|
||||||
DAILY_REPORT_KEYWORDS = ("日报", "晨报", "经营日报", "经营晨报")
|
DAILY_REPORT_KEYWORDS = ("日报", "晨报", "经营日报", "经营晨报")
|
||||||
PROJECT_WEEKLY_KEYWORDS = ("周报", "项目周报")
|
PROJECT_WEEKLY_KEYWORDS = ("周报", "项目周报")
|
||||||
ATTENDANCE_KEYWORDS = ("打卡", "考勤", "attendance")
|
ATTENDANCE_KEYWORDS = ("打卡", "考勤", "attendance")
|
||||||
RISK_KEYWORDS = ("风险", "预警", "risk")
|
RISK_KEYWORDS = ("风险", "预警", "risk")
|
||||||
AI_COMMAND_PREFIXES = ("问 ", "ai ", "AI ", "/ask ")
|
AI_COMMAND_PREFIXES = ("问 ", "ai ", "AI ", "/ask ")
|
||||||
RISK_TITLE = "风险预警"
|
|
||||||
DEFAULT_AI_PROMPT = "请说明你能做什么。"
|
DEFAULT_AI_PROMPT = "请说明你能做什么。"
|
||||||
RULE_TITLE = "AI 学习规则"
|
RULE_TITLE = "AI 学习规则"
|
||||||
RULE_CREATE_PATTERN = re.compile(r"^学习规则(?:\s+(\d{1,3}))?\s*[::]\s*(.*)$")
|
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):
|
if any(keyword in command_text for keyword in RISK_KEYWORDS):
|
||||||
summary = RiskService(self.db).summary()
|
report = ReportService(self.db).risk_progress()
|
||||||
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])}",
|
|
||||||
]
|
|
||||||
if auto_reply:
|
if auto_reply:
|
||||||
provider_response = self._send_card_if_configured(
|
provider_response = self._send_card_if_configured(
|
||||||
chat_id,
|
chat_id,
|
||||||
RISK_TITLE,
|
report[ReportResponseKey.TITLE],
|
||||||
lines,
|
report[ReportResponseKey.LINES],
|
||||||
actor,
|
actor,
|
||||||
)
|
)
|
||||||
return _command_result(
|
return _command_result(
|
||||||
FeishuCommandName.RISK_SUMMARY,
|
FeishuCommandName.RISK_SUMMARY,
|
||||||
FeishuReplyType.CARD,
|
FeishuReplyType.CARD,
|
||||||
RISK_TITLE,
|
report[ReportResponseKey.TITLE],
|
||||||
"\n".join(lines),
|
report[ReportResponseKey.CONTENT],
|
||||||
provider_response,
|
provider_response,
|
||||||
lines,
|
report[ReportResponseKey.LINES],
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt = command_text
|
prompt = command_text
|
||||||
|
|||||||
@@ -6,10 +6,20 @@ class ReportType(StrEnum):
|
|||||||
WEEKLY = "weekly"
|
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):
|
class ReportTitle(StrEnum):
|
||||||
DAILY_BRIEF = "每日经营晨报"
|
DAILY_BRIEF = "每日经营晨报"
|
||||||
PROJECT_WEEKLY = "项目周报"
|
PROJECT_WEEKLY = "项目周报"
|
||||||
ATTENDANCE_SUMMARY = "打卡汇总"
|
ATTENDANCE_SUMMARY = "打卡汇总"
|
||||||
|
RISK_PROGRESS = "项目风险进度"
|
||||||
WORK_DAILY = "经营日报"
|
WORK_DAILY = "经营日报"
|
||||||
WORK_WEEKLY = "经营周报"
|
WORK_WEEKLY = "经营周报"
|
||||||
PROJECT_LIFECYCLE = "项目全生命周期报告"
|
PROJECT_LIFECYCLE = "项目全生命周期报告"
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ from fastapi import APIRouter, Depends
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.background.task_queue import (
|
from app.core.background.task_queue import (
|
||||||
|
enqueue_attendance_summary_push,
|
||||||
enqueue_daily_brief_push,
|
enqueue_daily_brief_push,
|
||||||
enqueue_lifecycle_report,
|
enqueue_lifecycle_report,
|
||||||
enqueue_project_weekly_push,
|
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.database import get_db
|
||||||
from app.core.security import ApiPrincipal, require_api_key, require_operations_enabled
|
from app.core.security import ApiPrincipal, require_api_key, require_operations_enabled
|
||||||
@@ -36,6 +40,13 @@ def project_weekly(
|
|||||||
return ReportService(db).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")
|
@router.get("/project-lifecycle")
|
||||||
def project_lifecycle_report(
|
def project_lifecycle_report(
|
||||||
project_code: str | None = None,
|
project_code: str | None = None,
|
||||||
@@ -175,8 +186,9 @@ def push_daily_brief(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
principal: ApiPrincipal = Depends(require_api_key),
|
principal: ApiPrincipal = Depends(require_api_key),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
report = ReportService(db).daily_brief()
|
service = ReportService(db)
|
||||||
return ReportService(db).push_report(
|
report = service.daily_brief()
|
||||||
|
return service.push_report(
|
||||||
report,
|
report,
|
||||||
payload.receive_id,
|
payload.receive_id,
|
||||||
payload.receive_id_type,
|
payload.receive_id_type,
|
||||||
@@ -190,8 +202,73 @@ def push_project_weekly(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
principal: ApiPrincipal = Depends(require_api_key),
|
principal: ApiPrincipal = Depends(require_api_key),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
report = ReportService(db).project_weekly()
|
service = ReportService(db)
|
||||||
return ReportService(db).push_report(
|
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,
|
report,
|
||||||
payload.receive_id,
|
payload.receive_id,
|
||||||
payload.receive_id_type,
|
payload.receive_id_type,
|
||||||
@@ -221,3 +298,51 @@ def enqueue_project_weekly(
|
|||||||
receive_id_type=payload.receive_id_type,
|
receive_id_type=payload.receive_id_type,
|
||||||
actor=principal.actor,
|
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,
|
WorkTask,
|
||||||
)
|
)
|
||||||
from app.modules.reports.constants import (
|
from app.modules.reports.constants import (
|
||||||
|
ReportPushType,
|
||||||
ReportResponseKey,
|
ReportResponseKey,
|
||||||
ReportTitle,
|
ReportTitle,
|
||||||
)
|
)
|
||||||
@@ -65,6 +66,7 @@ class ReportSummaryMixin:
|
|||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
ReportResponseKey.TITLE: ReportTitle.DAILY_BRIEF,
|
ReportResponseKey.TITLE: ReportTitle.DAILY_BRIEF,
|
||||||
|
ReportResponseKey.REPORT_TYPE: ReportPushType.DAILY_BRIEF,
|
||||||
ReportResponseKey.LINES: lines,
|
ReportResponseKey.LINES: lines,
|
||||||
ReportResponseKey.CONTENT: "\n".join(lines),
|
ReportResponseKey.CONTENT: "\n".join(lines),
|
||||||
}
|
}
|
||||||
@@ -93,6 +95,28 @@ class ReportSummaryMixin:
|
|||||||
lines.append(f" - 超预算:{item.get('code')} {item.get('name')}")
|
lines.append(f" - 超预算:{item.get('code')} {item.get('name')}")
|
||||||
return {
|
return {
|
||||||
ReportResponseKey.TITLE: ReportTitle.PROJECT_WEEKLY,
|
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.LINES: lines,
|
||||||
ReportResponseKey.CONTENT: "\n".join(lines),
|
ReportResponseKey.CONTENT: "\n".join(lines),
|
||||||
}
|
}
|
||||||
@@ -122,6 +146,7 @@ class ReportSummaryMixin:
|
|||||||
lines.append(f"- {status}:{count}")
|
lines.append(f"- {status}:{count}")
|
||||||
return {
|
return {
|
||||||
ReportResponseKey.TITLE: ReportTitle.ATTENDANCE_SUMMARY,
|
ReportResponseKey.TITLE: ReportTitle.ATTENDANCE_SUMMARY,
|
||||||
|
ReportResponseKey.REPORT_TYPE: ReportPushType.ATTENDANCE_SUMMARY,
|
||||||
ReportResponseKey.WORK_DATE: target_date.isoformat(),
|
ReportResponseKey.WORK_DATE: target_date.isoformat(),
|
||||||
ReportResponseKey.TOTAL: total,
|
ReportResponseKey.TOTAL: total,
|
||||||
ReportResponseKey.ABNORMAL_TOTAL: abnormal_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.events.services import EventService
|
||||||
from app.modules.reports.constants import (
|
from app.modules.reports.constants import (
|
||||||
|
ReportPushType,
|
||||||
ReportResponseKey,
|
ReportResponseKey,
|
||||||
ReportTitle,
|
ReportTitle,
|
||||||
ReportType,
|
ReportType,
|
||||||
@@ -41,6 +42,34 @@ from app.modules.reports.services.common import _json_safe, _next_code
|
|||||||
|
|
||||||
|
|
||||||
class ReportWorkReportMixin:
|
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(
|
def generate_work_report(
|
||||||
self,
|
self,
|
||||||
report_type: str = ReportType.DAILY,
|
report_type: str = ReportType.DAILY,
|
||||||
@@ -119,6 +148,23 @@ class ReportWorkReportMixin:
|
|||||||
|
|
||||||
return {ReportResponseKey.REPORT: report, ReportResponseKey.DATA: record_data}
|
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(
|
def _resolve_period(
|
||||||
self,
|
self,
|
||||||
report_type: str,
|
report_type: str,
|
||||||
|
|||||||
@@ -1,46 +1,102 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from app.core.background.task_queue.constants import (
|
||||||
|
TASK_PUSH_ATTENDANCE_SUMMARY,
|
||||||
|
TASK_PUSH_DAILY_BRIEF,
|
||||||
|
TASK_PUSH_PROJECT_WEEKLY,
|
||||||
|
TASK_PUSH_RISK_PROGRESS,
|
||||||
|
TASK_PUSH_WORK_DAILY,
|
||||||
|
TASK_PUSH_WORK_WEEKLY,
|
||||||
|
)
|
||||||
from app.core.constants import ActorValue
|
from app.core.constants import ActorValue
|
||||||
from app.core.database import SessionLocal
|
from app.core.database import SessionLocal
|
||||||
from app.modules.feishu.constants import FeishuReceiveIdType
|
from app.modules.feishu.constants import FeishuReceiveIdType
|
||||||
from app.tasks.app import celery_app
|
from app.tasks.app import celery_app
|
||||||
|
|
||||||
|
ReportBuilder = Callable[[Any, str], dict[str, Any]]
|
||||||
|
|
||||||
@celery_app.task(name="reports.push_daily_brief")
|
|
||||||
|
@celery_app.task(name=TASK_PUSH_DAILY_BRIEF)
|
||||||
def push_daily_brief(
|
def push_daily_brief(
|
||||||
receive_id: str | None = None,
|
receive_id: str | None = None,
|
||||||
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
||||||
actor: str = ActorValue.SCHEDULER,
|
actor: str = ActorValue.SCHEDULER,
|
||||||
push_run_code: str | None = None,
|
push_run_code: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
from app.modules.reports.services import ReportService
|
return _push_report(_build_daily_brief, receive_id, receive_id_type, actor, push_run_code)
|
||||||
|
|
||||||
db = SessionLocal()
|
|
||||||
try:
|
|
||||||
report = ReportService(db).daily_brief()
|
|
||||||
return ReportService(db).push_report(
|
|
||||||
report,
|
|
||||||
receive_id,
|
|
||||||
receive_id_type,
|
|
||||||
actor,
|
|
||||||
push_run_code=push_run_code,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
@celery_app.task(name="reports.push_project_weekly")
|
@celery_app.task(name=TASK_PUSH_PROJECT_WEEKLY)
|
||||||
def push_project_weekly(
|
def push_project_weekly(
|
||||||
receive_id: str | None = None,
|
receive_id: str | None = None,
|
||||||
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
||||||
actor: str = ActorValue.SCHEDULER,
|
actor: str = ActorValue.SCHEDULER,
|
||||||
push_run_code: str | None = None,
|
push_run_code: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return _push_report(_build_project_weekly, receive_id, receive_id_type, actor, push_run_code)
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(name=TASK_PUSH_ATTENDANCE_SUMMARY)
|
||||||
|
def push_attendance_summary(
|
||||||
|
receive_id: str | None = None,
|
||||||
|
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
||||||
|
actor: str = ActorValue.SCHEDULER,
|
||||||
|
push_run_code: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return _push_report(
|
||||||
|
_build_attendance_summary,
|
||||||
|
receive_id,
|
||||||
|
receive_id_type,
|
||||||
|
actor,
|
||||||
|
push_run_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(name=TASK_PUSH_RISK_PROGRESS)
|
||||||
|
def push_risk_progress(
|
||||||
|
receive_id: str | None = None,
|
||||||
|
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
||||||
|
actor: str = ActorValue.SCHEDULER,
|
||||||
|
push_run_code: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return _push_report(_build_risk_progress, receive_id, receive_id_type, actor, push_run_code)
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(name=TASK_PUSH_WORK_DAILY)
|
||||||
|
def push_work_daily(
|
||||||
|
receive_id: str | None = None,
|
||||||
|
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
||||||
|
actor: str = ActorValue.SCHEDULER,
|
||||||
|
push_run_code: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return _push_report(_build_work_daily, receive_id, receive_id_type, actor, push_run_code)
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(name=TASK_PUSH_WORK_WEEKLY)
|
||||||
|
def push_work_weekly(
|
||||||
|
receive_id: str | None = None,
|
||||||
|
receive_id_type: str = FeishuReceiveIdType.CHAT_ID,
|
||||||
|
actor: str = ActorValue.SCHEDULER,
|
||||||
|
push_run_code: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return _push_report(_build_work_weekly, receive_id, receive_id_type, actor, push_run_code)
|
||||||
|
|
||||||
|
|
||||||
|
def _push_report(
|
||||||
|
build_report: ReportBuilder,
|
||||||
|
receive_id: str | None,
|
||||||
|
receive_id_type: str,
|
||||||
|
actor: str,
|
||||||
|
push_run_code: str | None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
from app.modules.reports.services import ReportService
|
from app.modules.reports.services import ReportService
|
||||||
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
report = ReportService(db).project_weekly()
|
service = ReportService(db)
|
||||||
return ReportService(db).push_report(
|
report = build_report(service, actor)
|
||||||
|
return service.push_report(
|
||||||
report,
|
report,
|
||||||
receive_id,
|
receive_id,
|
||||||
receive_id_type,
|
receive_id_type,
|
||||||
@@ -49,3 +105,31 @@ def push_project_weekly(
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_daily_brief(service: Any, actor: str) -> dict[str, Any]:
|
||||||
|
_ = actor
|
||||||
|
return service.daily_brief()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_project_weekly(service: Any, actor: str) -> dict[str, Any]:
|
||||||
|
_ = actor
|
||||||
|
return service.project_weekly()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_attendance_summary(service: Any, actor: str) -> dict[str, Any]:
|
||||||
|
_ = actor
|
||||||
|
return service.attendance_summary()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_risk_progress(service: Any, actor: str) -> dict[str, Any]:
|
||||||
|
_ = actor
|
||||||
|
return service.risk_progress()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_work_daily(service: Any, actor: str) -> dict[str, Any]:
|
||||||
|
return service.work_daily_report(reporter=actor, actor=actor)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_work_weekly(service: Any, actor: str) -> dict[str, Any]:
|
||||||
|
return service.work_weekly_report(reporter=actor, actor=actor)
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ from app.modules.reports.constants import (
|
|||||||
LifecycleSection,
|
LifecycleSection,
|
||||||
MetricKey,
|
MetricKey,
|
||||||
ReportPushStatus,
|
ReportPushStatus,
|
||||||
|
ReportPushType,
|
||||||
|
ReportResponseKey,
|
||||||
ReportTitle,
|
ReportTitle,
|
||||||
ReportType,
|
ReportType,
|
||||||
)
|
)
|
||||||
@@ -773,6 +775,72 @@ def test_new_ledgers_reports_and_risk_events() -> None:
|
|||||||
assert any(item["code"] == "TASK-RISK-001" for item in overdue_response.json()["items"])
|
assert any(item["code"] == "TASK-RISK-001" for item in overdue_response.json()["items"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_independent_feishu_report_schedules_and_tasks_are_registered() -> None:
|
||||||
|
from app.core.background.scheduler import create_scheduler
|
||||||
|
from app.core.background.task_queue.constants import (
|
||||||
|
TASK_PUSH_ATTENDANCE_SUMMARY,
|
||||||
|
TASK_PUSH_DAILY_BRIEF,
|
||||||
|
TASK_PUSH_PROJECT_WEEKLY,
|
||||||
|
TASK_PUSH_RISK_PROGRESS,
|
||||||
|
TASK_PUSH_WORK_DAILY,
|
||||||
|
TASK_PUSH_WORK_WEEKLY,
|
||||||
|
)
|
||||||
|
from app.tasks import celery_app
|
||||||
|
|
||||||
|
scheduler = create_scheduler()
|
||||||
|
job_ids = {job.id for job in scheduler.get_jobs()}
|
||||||
|
assert {
|
||||||
|
"daily_brief_push",
|
||||||
|
"project_weekly_push",
|
||||||
|
"attendance_summary_push",
|
||||||
|
"risk_progress_push",
|
||||||
|
"work_daily_push",
|
||||||
|
"work_weekly_push",
|
||||||
|
} <= job_ids
|
||||||
|
|
||||||
|
task_names = {
|
||||||
|
TASK_PUSH_DAILY_BRIEF,
|
||||||
|
TASK_PUSH_PROJECT_WEEKLY,
|
||||||
|
TASK_PUSH_ATTENDANCE_SUMMARY,
|
||||||
|
TASK_PUSH_RISK_PROGRESS,
|
||||||
|
TASK_PUSH_WORK_DAILY,
|
||||||
|
TASK_PUSH_WORK_WEEKLY,
|
||||||
|
}
|
||||||
|
assert task_names <= set(celery_app.tasks)
|
||||||
|
|
||||||
|
|
||||||
|
def test_independent_feishu_report_surfaces_are_available() -> None:
|
||||||
|
from app.modules.reports.services import ReportService
|
||||||
|
|
||||||
|
route_paths = {route.path for route in app.routes}
|
||||||
|
assert {
|
||||||
|
"/api/v1/reports/attendance-summary/push",
|
||||||
|
"/api/v1/reports/attendance-summary/enqueue",
|
||||||
|
"/api/v1/reports/risk-progress/push",
|
||||||
|
"/api/v1/reports/risk-progress/enqueue",
|
||||||
|
"/api/v1/reports/work-daily/push",
|
||||||
|
"/api/v1/reports/work-daily/enqueue",
|
||||||
|
"/api/v1/reports/work-weekly/push",
|
||||||
|
"/api/v1/reports/work-weekly/enqueue",
|
||||||
|
} <= route_paths
|
||||||
|
|
||||||
|
risk_response = client.get("/api/v1/reports/risk-progress", headers=headers)
|
||||||
|
assert risk_response.status_code == 200
|
||||||
|
assert risk_response.json()["title"] == ReportTitle.RISK_PROGRESS
|
||||||
|
assert risk_response.json()["report_type"] == ReportPushType.RISK_PROGRESS
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
service = ReportService(db)
|
||||||
|
work_daily = service.work_daily_report(reporter="pytest", actor="pytest")
|
||||||
|
work_weekly = service.work_weekly_report(reporter="pytest", actor="pytest")
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
assert work_daily[ReportResponseKey.REPORT_TYPE] == ReportPushType.WORK_DAILY
|
||||||
|
assert work_weekly[ReportResponseKey.REPORT_TYPE] == ReportPushType.WORK_WEEKLY
|
||||||
|
|
||||||
|
|
||||||
def test_project_lifecycle_report_summarizes_progress_cost_and_risk() -> None:
|
def test_project_lifecycle_report_summarizes_progress_cost_and_risk() -> None:
|
||||||
today = date.today()
|
today = date.today()
|
||||||
project_code = "P-LIFECYCLE-001"
|
project_code = "P-LIFECYCLE-001"
|
||||||
|
|||||||
Reference in New Issue
Block a user