274 lines
8.7 KiB
Python
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)}
|
|
)
|