Files
smart-project-pricing/后端服务/app/routers/profit.py
2026-01-31 21:33:06 +08:00

274 lines
8.7 KiB
Python

"""利润模拟路由
利润模拟测算相关的 API 接口
"""
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database import get_db
from app.models import ProfitSimulation, PricingPlan
from app.schemas.common import ResponseModel, PaginatedData, ErrorCode
from app.schemas.profit import (
PeriodType,
ProfitSimulationResponse,
ProfitSimulationListResponse,
SimulateProfitRequest,
SimulateProfitResponse,
SensitivityAnalysisRequest,
SensitivityAnalysisResponse,
BreakevenRequest,
BreakevenResponse,
)
from app.services.profit_service import ProfitService
router = APIRouter()
# 利润模拟 CRUD
@router.get("/profit-simulations", response_model=ResponseModel[PaginatedData[ProfitSimulationListResponse]])
async def list_profit_simulations(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
pricing_plan_id: Optional[int] = None,
period_type: Optional[PeriodType] = None,
sort_by: str = "created_at",
sort_order: str = "desc",
db: AsyncSession = Depends(get_db),
):
"""获取利润模拟列表"""
service = ProfitService(db)
simulations, total = await service.get_simulation_list(
pricing_plan_id=pricing_plan_id,
period_type=period_type,
page=page,
page_size=page_size,
)
items = []
for sim in simulations:
items.append(ProfitSimulationListResponse(
id=sim.id,
pricing_plan_id=sim.pricing_plan_id,
plan_name=sim.pricing_plan.plan_name if sim.pricing_plan else None,
project_name=sim.pricing_plan.project.project_name if sim.pricing_plan and sim.pricing_plan.project else None,
simulation_name=sim.simulation_name,
price=float(sim.price),
estimated_volume=sim.estimated_volume,
period_type=sim.period_type,
estimated_profit=float(sim.estimated_profit),
profit_margin=float(sim.profit_margin),
created_at=sim.created_at,
))
return ResponseModel(data=PaginatedData(
items=items,
total=total,
page=page,
page_size=page_size,
total_pages=(total + page_size - 1) // page_size,
))
@router.get("/profit-simulations/{simulation_id}", response_model=ResponseModel[ProfitSimulationResponse])
async def get_profit_simulation(
simulation_id: int,
db: AsyncSession = Depends(get_db),
):
"""获取模拟详情"""
result = await db.execute(
select(ProfitSimulation).options(
selectinload(ProfitSimulation.pricing_plan).selectinload(PricingPlan.project),
selectinload(ProfitSimulation.creator),
).where(ProfitSimulation.id == simulation_id)
)
sim = result.scalar_one_or_none()
if not sim:
raise HTTPException(
status_code=404,
detail={"code": ErrorCode.NOT_FOUND, "message": "模拟记录不存在"}
)
return ResponseModel(data=ProfitSimulationResponse(
id=sim.id,
pricing_plan_id=sim.pricing_plan_id,
plan_name=sim.pricing_plan.plan_name if sim.pricing_plan else None,
project_name=sim.pricing_plan.project.project_name if sim.pricing_plan and sim.pricing_plan.project else None,
simulation_name=sim.simulation_name,
price=float(sim.price),
estimated_volume=sim.estimated_volume,
period_type=sim.period_type,
estimated_revenue=float(sim.estimated_revenue),
estimated_cost=float(sim.estimated_cost),
estimated_profit=float(sim.estimated_profit),
profit_margin=float(sim.profit_margin),
breakeven_volume=sim.breakeven_volume,
created_at=sim.created_at,
created_by_name=sim.creator.username if sim.creator else None,
))
@router.delete("/profit-simulations/{simulation_id}", response_model=ResponseModel)
async def delete_profit_simulation(
simulation_id: int,
db: AsyncSession = Depends(get_db),
):
"""删除模拟记录"""
service = ProfitService(db)
deleted = await service.delete_simulation(simulation_id)
if not deleted:
raise HTTPException(
status_code=404,
detail={"code": ErrorCode.NOT_FOUND, "message": "模拟记录不存在"}
)
await db.commit()
return ResponseModel(message="删除成功")
# 执行模拟
@router.post("/pricing-plans/{plan_id}/simulate-profit", response_model=ResponseModel[SimulateProfitResponse])
async def simulate_profit(
plan_id: int,
request: SimulateProfitRequest,
db: AsyncSession = Depends(get_db),
):
"""执行利润模拟"""
service = ProfitService(db)
try:
response = await service.simulate_profit(
pricing_plan_id=plan_id,
price=request.price,
estimated_volume=request.estimated_volume,
period_type=request.period_type,
)
await db.commit()
return ResponseModel(data=response)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# 敏感性分析
@router.post("/profit-simulations/{simulation_id}/sensitivity", response_model=ResponseModel[SensitivityAnalysisResponse])
async def create_sensitivity_analysis(
simulation_id: int,
request: SensitivityAnalysisRequest,
db: AsyncSession = Depends(get_db),
):
"""执行敏感性分析"""
service = ProfitService(db)
try:
response = await service.sensitivity_analysis(
simulation_id=simulation_id,
price_change_rates=request.price_change_rates,
)
await db.commit()
return ResponseModel(data=response)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/profit-simulations/{simulation_id}/sensitivity", response_model=ResponseModel[SensitivityAnalysisResponse])
async def get_sensitivity_analysis(
simulation_id: int,
db: AsyncSession = Depends(get_db),
):
"""获取敏感性分析结果"""
result = await db.execute(
select(ProfitSimulation).options(
selectinload(ProfitSimulation.sensitivity_analyses),
selectinload(ProfitSimulation.pricing_plan),
).where(ProfitSimulation.id == simulation_id)
)
sim = result.scalar_one_or_none()
if not sim:
raise HTTPException(
status_code=404,
detail={"code": ErrorCode.NOT_FOUND, "message": "模拟记录不存在"}
)
if not sim.sensitivity_analyses:
raise HTTPException(
status_code=404,
detail={"code": ErrorCode.NOT_FOUND, "message": "尚未执行敏感性分析"}
)
from app.schemas.profit import SensitivityResultItem
results = [
SensitivityResultItem(
price_change_rate=float(sa.price_change_rate),
adjusted_price=float(sa.adjusted_price),
adjusted_profit=float(sa.adjusted_profit),
profit_change_rate=float(sa.profit_change_rate),
)
for sa in sorted(sim.sensitivity_analyses, key=lambda x: x.price_change_rate)
]
return ResponseModel(data=SensitivityAnalysisResponse(
simulation_id=simulation_id,
base_price=float(sim.price),
base_profit=float(sim.estimated_profit),
sensitivity_results=results,
))
# 盈亏平衡分析
@router.get("/pricing-plans/{plan_id}/breakeven", response_model=ResponseModel[BreakevenResponse])
async def get_breakeven_analysis(
plan_id: int,
target_profit: Optional[float] = Query(None, description="目标利润"),
db: AsyncSession = Depends(get_db),
):
"""获取盈亏平衡分析"""
service = ProfitService(db)
try:
response = await service.breakeven_analysis(
pricing_plan_id=plan_id,
target_profit=target_profit,
)
return ResponseModel(data=response)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# AI 利润预测
@router.post("/profit-simulations/{simulation_id}/forecast", response_model=ResponseModel[dict])
async def generate_profit_forecast(
simulation_id: int,
db: AsyncSession = Depends(get_db),
):
"""AI 生成利润预测分析"""
service = ProfitService(db)
try:
content = await service.generate_profit_forecast(simulation_id)
return ResponseModel(data={"content": content})
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(
status_code=500,
detail={"code": ErrorCode.AI_SERVICE_ERROR, "message": str(e)}
)