```
feat(ai_agent): 新增openclaw_hermes混合AI适配器 新增OpenClawHermesAdapter适配器,结合Hermes记忆功能和OpenClaw执行能力, 实现AI问答流程中的记忆召回、执行操作和记忆存储的完整闭环。 同时更新NoopAdapter提示信息,添加新的模型提供商选项。 feat(business): 新增考勤、工作报告和风险事件业务模型 新增AttendanceRecord、WorkReport、RiskEvent和LegacySyncRun四个业务模型, 扩展业务领域注册表,支持考勤管理、工作报告生成和风险事件跟踪等核心业务功能。 feat(reports): 实现考勤汇总和工作日报周报生成功能 新增attendance_summary方法用于统计每日考勤情况, 新增generate_work_report方法用于生成日/周经营报告, 包含任务完成情况、待处理事项和风险指标等综合信息。 feat(risk): 扩展风险管理API端点和供应商风险检测 新增供应商风险查询端点和风险事件管理端点, 提供风险事件列表查询和自动生成功能, 增强供应商风险评估能力。 feat(feishu): 添加考勤查询命令和风险摘要增强 集成考勤汇总查询功能到飞书命令系统, 在风险摘要中添加供应商风险和开放风险事件统计, 丰富日常经营管理信息展示。 refactor(service): 优化业务服务数据验证和类型转换 重构_model_payload函数实现数据验证和类型转换, 添加列值类型强制转换逻辑,提高API数据处理的准确性和安全性。 build(deps): 添加postgresql数据库驱动依赖 在Dockerfile中添加psycopg[binary]==3.2.3依赖包, 支持PostgreSQL数据库连接和操作。 chore(config): 更新.gitignore文件排除备份和迁移目录 在.gitignore中添加AGENTS.md.bak-*和migration/目录排除规则, 避免备份文件和本地迁移工作区被提交到版本控制系统。 ```
This commit is contained in:
@@ -1,12 +1,30 @@
|
||||
from datetime import date, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.business.models import Expense, FundAccount, Procurement, Project, WorkTask
|
||||
from app.modules.audit.schemas import AuditLogCreate
|
||||
from app.modules.audit.service import AuditService
|
||||
from app.modules.business.models import (
|
||||
AttendanceRecord,
|
||||
Expense,
|
||||
FundAccount,
|
||||
Procurement,
|
||||
Project,
|
||||
RiskEvent,
|
||||
WorkReport,
|
||||
WorkTask,
|
||||
)
|
||||
from app.modules.business.service import serialize_model
|
||||
from app.modules.feishu.service import FeishuService
|
||||
from app.modules.risk.service import RiskService
|
||||
|
||||
DONE_STATUSES = {"完成", "已完成", "关闭", "done", "completed", "closed"}
|
||||
PENDING_APPROVAL_STATUSES = {"草稿", "审批中", "待审批", "pending"}
|
||||
PROJECT_CLOSED_STATUSES = {"验收", "已完成", "复盘", "归档", "关闭", "closed"}
|
||||
|
||||
|
||||
def _money(value: Decimal | int | float | None) -> str:
|
||||
"""Format a numeric value as a two-decimal money string."""
|
||||
@@ -15,6 +33,26 @@ def _money(value: Decimal | int | float | None) -> str:
|
||||
return f"{amount:,.2f}"
|
||||
|
||||
|
||||
def _json_safe(value: Any) -> Any:
|
||||
"""Convert nested report payloads into JSON-storable values."""
|
||||
|
||||
if isinstance(value, Decimal):
|
||||
return float(value)
|
||||
if isinstance(value, (datetime, date)):
|
||||
return value.isoformat()
|
||||
if isinstance(value, list):
|
||||
return [_json_safe(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {key: _json_safe(item) for key, item in value.items()}
|
||||
return value
|
||||
|
||||
|
||||
def _next_code(prefix: str) -> str:
|
||||
"""Build a compact unique code for generated report records."""
|
||||
|
||||
return f"{prefix}-{datetime.utcnow():%Y%m%d%H%M%S%f}"
|
||||
|
||||
|
||||
class ReportService:
|
||||
"""Build operational reports and push them through Feishu."""
|
||||
|
||||
@@ -22,61 +60,59 @@ class ReportService:
|
||||
self.db = db
|
||||
self.risks = RiskService(db)
|
||||
|
||||
def _count(self, model: type, *conditions: Any) -> int:
|
||||
stmt = select(func.count()).select_from(model)
|
||||
for condition in conditions:
|
||||
stmt = stmt.where(condition)
|
||||
return int(self.db.execute(stmt).scalar() or 0)
|
||||
|
||||
def daily_brief(self) -> dict:
|
||||
project_count = int(
|
||||
self.db.execute(select(func.count()).select_from(Project)).scalar() or 0
|
||||
project_count = self._count(Project)
|
||||
task_count = self._count(WorkTask)
|
||||
procurement_pending = self._count(
|
||||
Procurement,
|
||||
Procurement.approval_status.in_(PENDING_APPROVAL_STATUSES),
|
||||
)
|
||||
task_count = int(self.db.execute(select(func.count()).select_from(WorkTask)).scalar() or 0)
|
||||
procurement_pending = int(
|
||||
self.db.execute(
|
||||
select(func.count())
|
||||
.select_from(Procurement)
|
||||
.where(Procurement.approval_status.in_(["草稿", "审批中", "待审批"]))
|
||||
).scalar()
|
||||
or 0
|
||||
)
|
||||
expense_pending = int(
|
||||
self.db.execute(
|
||||
select(func.count())
|
||||
.select_from(Expense)
|
||||
.where(Expense.approval_status.in_(["草稿", "审批中", "待审批"]))
|
||||
).scalar()
|
||||
or 0
|
||||
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['total']},异常:{attendance['abnormal_total']}",
|
||||
f"- 逾期任务:{len(risk_summary['overdue_tasks'])}",
|
||||
f"- 延期项目:{len(risk_summary['delayed_projects'])}",
|
||||
f"- 超预算项目:{len(risk_summary['over_budget_projects'])}",
|
||||
f"- 资金风险账户:{len(risk_summary['fund_risks'])}",
|
||||
f"- 供应商风险:{len(risk_summary['supplier_risks'])}",
|
||||
f"- 打开风险事件:{len(risk_summary['open_events'])}",
|
||||
f"- 综合风险等级:{risk_summary['risk_level']}",
|
||||
]
|
||||
return {"title": "每日经营晨报", "lines": lines, "content": "\n".join(lines)}
|
||||
|
||||
def project_weekly(self) -> dict:
|
||||
active = int(
|
||||
self.db.execute(
|
||||
select(func.count())
|
||||
.select_from(Project)
|
||||
.where(Project.status.notin_(["验收", "已完成", "复盘", "归档", "关闭"]))
|
||||
).scalar()
|
||||
or 0
|
||||
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="open")
|
||||
lines = [
|
||||
f"- 活跃项目:{active}",
|
||||
f"- 延期项目:{len(delayed)}",
|
||||
f"- 超预算项目:{len(over_budget)}",
|
||||
f"- 打开风险事件:{len(open_risks)}",
|
||||
"- 需要管理层关注:",
|
||||
]
|
||||
for item in delayed[:10]:
|
||||
@@ -85,6 +121,184 @@ class ReportService:
|
||||
lines.append(f" - 超预算:{item.get('code')} {item.get('name')}")
|
||||
return {"title": "项目周报", "lines": lines, "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 {"迟到", "早退", "缺卡", "旷工", "异常"}
|
||||
)
|
||||
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 {
|
||||
"title": "打卡汇总",
|
||||
"work_date": target_date.isoformat(),
|
||||
"total": total,
|
||||
"abnormal_total": abnormal_total,
|
||||
"status_counts": status_counts,
|
||||
"lines": lines,
|
||||
"content": "\n".join(lines),
|
||||
}
|
||||
|
||||
def generate_work_report(
|
||||
self,
|
||||
report_type: str = "daily",
|
||||
reporter: str = "system",
|
||||
department: str | None = None,
|
||||
project_code: str | None = None,
|
||||
period_start: date | None = None,
|
||||
period_end: date | None = None,
|
||||
persist: bool = True,
|
||||
actor: str = "api",
|
||||
) -> dict[str, Any]:
|
||||
"""Generate a daily or weekly operating report, optionally persisting it."""
|
||||
|
||||
start, end = self._resolve_period(report_type, period_start, period_end)
|
||||
metrics = self._report_metrics(start, end, project_code, department)
|
||||
risk_summary = _json_safe(self.risks.summary())
|
||||
title = "经营日报" if report_type == "daily" else "经营周报"
|
||||
lines = self._work_report_lines(title, start, end, metrics, risk_summary)
|
||||
report = {
|
||||
"title": title,
|
||||
"report_type": report_type,
|
||||
"period_start": start.isoformat(),
|
||||
"period_end": end.isoformat(),
|
||||
"lines": lines,
|
||||
"content": "\n".join(lines),
|
||||
"metrics": metrics,
|
||||
"risk_summary": risk_summary,
|
||||
}
|
||||
|
||||
record_data = None
|
||||
if persist:
|
||||
record = WorkReport(
|
||||
code=_next_code(f"REPORT-{report_type.upper()}"),
|
||||
report_type=report_type,
|
||||
title=title,
|
||||
reporter=reporter,
|
||||
department=department,
|
||||
project_code=project_code,
|
||||
period_start=start,
|
||||
period_end=end,
|
||||
content=report["content"],
|
||||
metrics=metrics,
|
||||
risk_summary=risk_summary,
|
||||
)
|
||||
self.db.add(record)
|
||||
self.db.commit()
|
||||
self.db.refresh(record)
|
||||
record_data = serialize_model(record)
|
||||
AuditService(self.db).log(
|
||||
AuditLogCreate(
|
||||
actor=actor,
|
||||
source="reports",
|
||||
action=f"generate_{report_type}_report",
|
||||
target_type="work-reports",
|
||||
target_id=str(record.id),
|
||||
response_payload=record_data,
|
||||
)
|
||||
)
|
||||
|
||||
return {"report": report, "data": record_data}
|
||||
|
||||
def _resolve_period(
|
||||
self,
|
||||
report_type: str,
|
||||
period_start: date | None,
|
||||
period_end: date | None,
|
||||
) -> tuple[date, date]:
|
||||
today = date.today()
|
||||
if report_type == "daily":
|
||||
start = period_start or period_end or today
|
||||
return start, period_end or start
|
||||
end = period_end or today
|
||||
start = period_start or end - timedelta(days=6)
|
||||
return start, end
|
||||
|
||||
def _report_metrics(
|
||||
self,
|
||||
start: date,
|
||||
end: date,
|
||||
project_code: str | None,
|
||||
department: str | None,
|
||||
) -> dict[str, Any]:
|
||||
task_filters = [
|
||||
WorkTask.due_date.is_not(None),
|
||||
WorkTask.due_date >= start,
|
||||
WorkTask.due_date <= end,
|
||||
]
|
||||
procurement_filters = [Procurement.approval_status.in_(PENDING_APPROVAL_STATUSES)]
|
||||
expense_filters = [Expense.approval_status.in_(PENDING_APPROVAL_STATUSES)]
|
||||
attendance_filters = [
|
||||
AttendanceRecord.work_date >= start,
|
||||
AttendanceRecord.work_date <= end,
|
||||
]
|
||||
if project_code:
|
||||
task_filters.append(WorkTask.project_code == project_code)
|
||||
procurement_filters.append(Procurement.project_code == project_code)
|
||||
expense_filters.append(Expense.project_code == project_code)
|
||||
attendance_filters.append(AttendanceRecord.project_code == project_code)
|
||||
if department:
|
||||
expense_filters.append(Expense.department == department)
|
||||
attendance_filters.append(AttendanceRecord.department == department)
|
||||
|
||||
completed_tasks = self._count(WorkTask, WorkTask.status.in_(DONE_STATUSES), *task_filters)
|
||||
overdue_tasks = self._count(
|
||||
WorkTask,
|
||||
WorkTask.due_date < date.today(),
|
||||
WorkTask.status.notin_(DONE_STATUSES),
|
||||
*task_filters,
|
||||
)
|
||||
return {
|
||||
"projects_total": self._count(Project),
|
||||
"active_projects": self._count(
|
||||
Project,
|
||||
Project.status.notin_(PROJECT_CLOSED_STATUSES),
|
||||
),
|
||||
"tasks_total": self._count(WorkTask, *task_filters),
|
||||
"tasks_completed": completed_tasks,
|
||||
"tasks_overdue": overdue_tasks,
|
||||
"procurements_pending": self._count(Procurement, *procurement_filters),
|
||||
"expenses_pending": self._count(Expense, *expense_filters),
|
||||
"attendance_total": self._count(AttendanceRecord, *attendance_filters),
|
||||
"open_risk_events": self._count(RiskEvent, RiskEvent.status == "open"),
|
||||
}
|
||||
|
||||
def _work_report_lines(
|
||||
self,
|
||||
title: str,
|
||||
start: date,
|
||||
end: date,
|
||||
metrics: dict[str, Any],
|
||||
risk_summary: dict[str, Any],
|
||||
) -> list[str]:
|
||||
return [
|
||||
f"- 报告:{title}",
|
||||
f"- 周期:{start.isoformat()} 至 {end.isoformat()}",
|
||||
f"- 项目:总数 {metrics['projects_total']},活跃 {metrics['active_projects']}",
|
||||
f"- 任务:总数 {metrics['tasks_total']},完成 {metrics['tasks_completed']}",
|
||||
f"- 逾期任务:{metrics['tasks_overdue']}",
|
||||
f"- 待处理采购:{metrics['procurements_pending']}",
|
||||
f"- 待处理费用:{metrics['expenses_pending']}",
|
||||
f"- 打卡记录:{metrics['attendance_total']}",
|
||||
f"- 打开风险事件:{metrics['open_risk_events']}",
|
||||
f"- 综合风险等级:{risk_summary['risk_level']}",
|
||||
]
|
||||
|
||||
def push_report(
|
||||
self,
|
||||
report: dict,
|
||||
|
||||
Reference in New Issue
Block a user