Files
X-Financial/server/src/app/services/hermes_expense_report.py
caoxiaozhu 50b1c3f9a9 feat: 增强规则资产管理与审计页面运行时调试
后端新增规则资产版本管理和规则文件 CRUD 接口,优化风险
规则生成模板执行和员工数据模型字段,知识库 RAG 增强本
地回退和文档提取能力,清理旧风险规则文件统一由生成引擎
管理,前端审计页面增加运行时调试面板和规则资产编辑交互,
补充单元测试覆盖。
2026-05-24 21:44:17 +08:00

105 lines
4.0 KiB
Python
Raw 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 __future__ import annotations
import json
from datetime import datetime, timedelta, timezone
from typing import Any
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.core.logging import get_logger
from app.models.financial_record import ExpenseClaim
from app.services.runtime_chat import RuntimeChatService
logger = get_logger("app.services.hermes_expense_report")
class HermesExpenseReportService:
def __init__(self, db: Session) -> None:
self.db = db
self.chat_service = RuntimeChatService(db)
def generate_weekly_report(self, log_id: str | None = None) -> None:
logger.info("Starting Hermes weekly expense report generation...")
# 1. 聚合数据
aggregated_data = self._aggregate_recent_expenses(days=7)
if not aggregated_data.get("total_amount"):
logger.info("No expense data in the last 7 days. Skipping report.")
return
# 2. 传入大模型分析
report_markdown = self._generate_insights_with_llm(aggregated_data)
if not report_markdown:
logger.warning("Failed to generate expense report from LLM.")
return
# 3. 模拟发送报告
self._deliver_report(report_markdown, log_id)
logger.info("Hermes weekly expense report generation completed.")
def _aggregate_recent_expenses(self, days: int = 7) -> dict[str, Any]:
target_date = datetime.now(timezone.utc) - timedelta(days=days)
# 基础过滤最近N天且不是驳回状态的单据
base_filter = [
ExpenseClaim.occurred_at >= target_date,
ExpenseClaim.status != "rejected"
]
# 1. 按部门汇总
dept_stmt = select(
ExpenseClaim.department_name,
func.sum(ExpenseClaim.amount).label("total")
).where(*base_filter).group_by(ExpenseClaim.department_name)
dept_results = self.db.execute(dept_stmt).all()
by_department = {row.department_name or "Unknown": float(row.total or 0) for row in dept_results}
# 2. 按类目汇总
type_stmt = select(
ExpenseClaim.expense_type,
func.sum(ExpenseClaim.amount).label("total")
).where(*base_filter).group_by(ExpenseClaim.expense_type)
type_results = self.db.execute(type_stmt).all()
by_expense_type = {row.expense_type or "Unknown": float(row.total or 0) for row in type_results}
# 3. 总花费
total_amount = sum(by_department.values())
return {
"period": f"Last {days} days",
"total_amount": total_amount,
"by_department": by_department,
"by_expense_type": by_expense_type
}
def _generate_insights_with_llm(self, data: dict[str, Any]) -> str | None:
system_prompt = (
"你是公司的财务分析专家。请根据提供的最近期业务开销数据,撰写一份简洁有力的【高管费控洞察周报】。\n"
"要求:\n"
"1. 不要机械地罗列数字,要像人一样指出异常(例如:哪个部门花钱最多?打车费是不是异常高?)。\n"
"2. 给出 1 条削减成本的实操建议。\n"
"3. 纯 Markdown 格式输出,不超过 300 字。"
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"开销统计数据:\n{json.dumps(data, ensure_ascii=False, indent=2)}"}
]
response = self.chat_service.complete(
messages,
max_tokens=800,
temperature=0.4
)
return response
def _deliver_report(self, report_markdown: str, log_id: str | None) -> None:
# TODO: 未来在这里接入企微/钉钉机器人或邮件发送接口
logger.info(f"\n================ Hermes Weekly Report [LogID: {log_id}] ================\n"
f"{report_markdown}\n"
f"==========================================================================")