Files
selfrelease 5278190750 feat: 凭证生成、成本分析、AI问答、前端页面、集成测试与E2E测试
- 后端: 凭证生成引擎、金蝶导出器、凭证模板服务
- 后端: 成本分析服务、AI问答服务
- 后端: 科目映射CRUD API、分析API、QA API
- 后端: 集成测试(认证/任务/凭证) 49个测试全部通过
- 前端: 凭证管理、成本分析、导出中心、知识库、系统设置页面
- 前端: AuthGuard认证守卫、Dashboard AI聊天功能
- 前端: Playwright E2E测试 16 passed, 1 skipped
- 基础设施: Docker Compose、Nginx反向代理、.env.example
- 文档: 用户手册、管理员手册、发布检查清单
2026-07-07 21:21:29 +08:00

242 lines
7.6 KiB
Python

"""
成本分析 API
提供人工成本分析、部门拆分、费用科目拆分、环比变化等接口
"""
import csv
import io
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.tenant import get_current_company_id
from app.services.analysis.cost_calculator import CostCalculatorService
from app.services.ai.cost_analyzer import CostAnalyzerService
router = APIRouter(prefix="/api/analysis", tags=["成本分析"])
class CostSummaryResponse(BaseModel):
"""成本汇总响应"""
total_cost: float
salary_cost: float
social_security_cost: float
fund_cost: float
employee_count: int
class DepartmentCostResponse(BaseModel):
"""部门成本响应"""
department: str
employee_count: int
salary_cost: float
social_security_cost: float
fund_cost: float
total_cost: float
class ExpenseBreakdownResponse(BaseModel):
"""费用科目拆分响应"""
expense_type: str
amount: float
class MonthOverMonthResponse(BaseModel):
"""环比变化响应"""
total_cost: Dict[str, Any]
salary_cost: Dict[str, Any]
social_security_cost: Dict[str, Any]
fund_cost: Dict[str, Any]
employee_count: Dict[str, Any]
class CostChangeAnalysisResponse(BaseModel):
"""成本变化分析响应"""
new_employees: List[Dict[str, Any]]
left_employees: List[Dict[str, Any]]
salary_adjustments: List[Dict[str, Any]]
new_employee_cost: float
left_employee_saving: float
adjustment_cost: float
net_change: float
class FullAnalysisResponse(BaseModel):
"""完整成本分析响应"""
summary: CostSummaryResponse
departments: List[DepartmentCostResponse]
expenses: List[ExpenseBreakdownResponse]
changes: Optional[Dict[str, Any]] = None
ai_summary: Optional[str] = None
@router.get("/labor-cost/{task_id}", response_model=FullAnalysisResponse)
async def get_labor_cost_analysis(
task_id: int,
prev_task_id: Optional[int] = Query(None, description="上月任务ID,用于环比"),
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> FullAnalysisResponse:
"""
获取人工成本分析
包含总额、部门拆分、费用科目拆分,可选环比
"""
service = CostCalculatorService(db)
summary = await service.calculate_total_cost(task_id)
departments = await service.calculate_by_department(task_id)
expenses = await service.calculate_by_expense_type(task_id)
changes = None
ai_summary = None
if prev_task_id:
changes = await service.calculate_month_over_month(task_id, prev_task_id)
change_detail = await service.analyze_cost_changes(task_id, prev_task_id)
# 尝试 AI 分析
try:
analyzer = CostAnalyzerService()
prev_summary = await service.calculate_total_cost(prev_task_id)
ai_summary = await analyzer.analyze_cost_changes(
summary.to_dict(),
prev_summary.to_dict(),
change_detail,
)
except Exception:
pass
return FullAnalysisResponse(
summary=CostSummaryResponse(**summary.to_dict()),
departments=[
DepartmentCostResponse(**d.to_dict()) for d in departments
],
expenses=[
ExpenseBreakdownResponse(expense_type=k, amount=v)
for k, v in expenses.items()
],
changes=changes,
ai_summary=ai_summary,
)
@router.get("/labor-cost/{task_id}/summary", response_model=CostSummaryResponse)
async def get_cost_summary(
task_id: int,
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> CostSummaryResponse:
"""获取成本汇总"""
service = CostCalculatorService(db)
summary = await service.calculate_total_cost(task_id)
return CostSummaryResponse(**summary.to_dict())
@router.get("/labor-cost/{task_id}/departments", response_model=List[DepartmentCostResponse])
async def get_department_costs(
task_id: int,
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> List[DepartmentCostResponse]:
"""获取部门成本拆分"""
service = CostCalculatorService(db)
departments = await service.calculate_by_department(task_id)
return [DepartmentCostResponse(**d.to_dict()) for d in departments]
@router.get("/labor-cost/{task_id}/expenses", response_model=List[ExpenseBreakdownResponse])
async def get_expense_breakdown(
task_id: int,
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> List[ExpenseBreakdownResponse]:
"""获取费用科目拆分"""
service = CostCalculatorService(db)
expenses = await service.calculate_by_expense_type(task_id)
return [
ExpenseBreakdownResponse(expense_type=k, amount=v)
for k, v in expenses.items()
]
@router.get("/labor-cost/{task_id}/changes", response_model=CostChangeAnalysisResponse)
async def get_cost_changes(
task_id: int,
prev_task_id: int = Query(..., description="上月任务ID"),
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> CostChangeAnalysisResponse:
"""获取成本变化分析"""
service = CostCalculatorService(db)
changes = await service.analyze_cost_changes(task_id, prev_task_id)
return CostChangeAnalysisResponse(**changes)
@router.get("/labor-cost/{task_id}/export")
async def export_cost_analysis(
task_id: int,
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> StreamingResponse:
"""
导出成本分析为 Excel
返回包含成本汇总、部门拆分、费用拆分的 Excel 文件
"""
service = CostCalculatorService(db)
summary = await service.calculate_total_cost(task_id)
departments = await service.calculate_by_department(task_id)
expenses = await service.calculate_by_expense_type(task_id)
# 使用 openpyxl 生成 Excel
from openpyxl import Workbook
wb = Workbook()
# Sheet1: 成本汇总
ws1 = wb.active
ws1.title = "成本汇总"
ws1.append(["项目", "金额(元)"])
ws1.append(["人工成本总额", summary.total_cost])
ws1.append(["工资成本", summary.salary_cost])
ws1.append(["社保成本(公司部分)", summary.social_security_cost])
ws1.append(["公积金成本(公司部分)", summary.fund_cost])
ws1.append(["员工人数", summary.employee_count])
# Sheet2: 部门拆分
ws2 = wb.create_sheet("部门拆分")
ws2.append(["部门", "人数", "工资成本", "社保成本", "公积金成本", "合计"])
for dept in departments:
ws2.append([
dept.department,
dept.employee_count,
dept.salary_cost,
dept.social_security_cost,
dept.fund_cost,
dept.total_cost,
])
# Sheet3: 费用科目拆分
ws3 = wb.create_sheet("费用科目拆分")
ws3.append(["费用科目", "金额(元)"])
for expense_type, amount in expenses.items():
ws3.append([expense_type, amount])
# 输出到内存
output = io.BytesIO()
wb.save(output)
output.seek(0)
filename = f"cost_analysis_{task_id}.xlsx"
return StreamingResponse(
output,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)