Files
AiTool/backend/app/routers/projects.py
丹尼尔 ad96272ab6 fix:bug
2026-03-12 19:35:06 +08:00

257 lines
8.2 KiB
Python

from pathlib import Path
from typing import Any, Dict
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from backend.app import models
from backend.app.db import get_db
from backend.app.schemas import (
ContractGenerateRequest,
ContractGenerateResponse,
ProjectRead,
ProjectUpdate,
QuoteGenerateResponse,
RequirementAnalyzeRequest,
RequirementAnalyzeResponse,
)
from backend.app.services.ai_service import analyze_requirement
from backend.app.services.doc_service import (
generate_contract_word,
generate_quote_excel,
generate_quote_pdf_from_data,
)
router = APIRouter(prefix="/projects", tags=["projects"])
def _build_markdown_from_analysis(data: Dict[str, Any]) -> str:
"""
Convert structured AI analysis JSON into a human-editable Markdown document.
"""
lines: list[str] = []
lines.append("# 项目方案草稿")
lines.append("")
total_hours = data.get("total_estimated_hours")
total_amount = data.get("total_amount")
if total_hours is not None or total_amount is not None:
lines.append("## 概要")
if total_hours is not None:
lines.append(f"- 建议总工时:**{total_hours}**")
if total_amount is not None:
lines.append(f"- 建议总报价:**{total_amount}**")
lines.append("")
modules = data.get("modules") or []
if modules:
lines.append("## 功能模块与技术实现")
for idx, module in enumerate(modules, start=1):
name = module.get("name", f"模块 {idx}")
desc = module.get("description") or ""
tech = module.get("technical_approach") or ""
hours = module.get("estimated_hours")
unit_price = module.get("unit_price")
subtotal = module.get("subtotal")
lines.append(f"### {idx}. {name}")
if desc:
lines.append(desc)
lines.append("")
if tech:
lines.append("**技术实现思路:**")
lines.append(tech)
lines.append("")
if hours is not None or unit_price is not None or subtotal is not None:
lines.append("**工时与报价:**")
if hours is not None:
lines.append(f"- 预估工时:{hours}")
if unit_price is not None:
lines.append(f"- 单价:{unit_price}")
if subtotal is not None:
lines.append(f"- 小计:{subtotal}")
lines.append("")
notes = data.get("notes")
if notes:
lines.append("## 备注")
lines.append(notes)
lines.append("")
return "\n".join(lines)
@router.get("/", response_model=list[ProjectRead])
async def list_projects(db: Session = Depends(get_db)):
projects = (
db.query(models.Project)
.order_by(models.Project.created_at.desc())
.all()
)
return projects
@router.get("/{project_id}", response_model=ProjectRead)
async def get_project(project_id: int, db: Session = Depends(get_db)):
project = db.query(models.Project).get(project_id)
if not project:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
return project
@router.patch("/{project_id}", response_model=ProjectRead)
async def update_project(
project_id: int,
payload: ProjectUpdate,
db: Session = Depends(get_db),
):
project = db.query(models.Project).get(project_id)
if not project:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
if payload.ai_solution_md is not None:
project.ai_solution_md = payload.ai_solution_md
if payload.status is not None:
project.status = payload.status
db.commit()
db.refresh(project)
return project
@router.post("/analyze", response_model=RequirementAnalyzeResponse)
async def analyze_project_requirement(
payload: RequirementAnalyzeRequest,
db: Session = Depends(get_db),
):
# Ensure customer exists
customer = db.query(models.Customer).get(payload.customer_id)
if not customer:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found")
analysis = await analyze_requirement(payload.raw_text)
ai_solution_md = _build_markdown_from_analysis(analysis)
project = models.Project(
customer_id=payload.customer_id,
raw_requirement=payload.raw_text,
ai_solution_md=ai_solution_md,
status="draft",
)
db.add(project)
db.commit()
db.refresh(project)
return RequirementAnalyzeResponse(
project_id=project.id,
ai_solution_md=ai_solution_md,
ai_solution_json=analysis,
)
@router.post("/{project_id}/generate_quote", response_model=QuoteGenerateResponse)
async def generate_project_quote(
project_id: int,
db: Session = Depends(get_db),
):
project = db.query(models.Project).get(project_id)
if not project:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
# Re-analyze to get fresh structured data (the UI will allow user to edit Markdown separately).
analysis = await analyze_requirement(project.raw_requirement)
# Paths
base_dir = Path("data/quotes")
base_dir.mkdir(parents=True, exist_ok=True)
excel_path = base_dir / f"quote_project_{project.id}.xlsx"
pdf_path = base_dir / f"quote_project_{project.id}.pdf"
template_path = Path("templates/quote_template.xlsx")
if not template_path.exists():
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Quote template file not found on server.",
)
await generate_quote_excel(analysis, str(template_path), str(excel_path))
await generate_quote_pdf_from_data(analysis, str(pdf_path))
total_amount = float(analysis.get("total_amount") or 0.0)
quote = models.Quote(
project_id=project.id,
total_amount=total_amount,
file_path=str(pdf_path),
)
db.add(quote)
db.commit()
db.refresh(quote)
return QuoteGenerateResponse(
quote_id=quote.id,
project_id=project.id,
total_amount=total_amount,
excel_path=str(excel_path),
pdf_path=str(pdf_path),
)
@router.post(
"/{project_id}/generate_contract",
response_model=ContractGenerateResponse,
)
async def generate_project_contract(
project_id: int,
payload: ContractGenerateRequest,
db: Session = Depends(get_db),
):
project = db.query(models.Project).get(project_id)
if not project:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
customer = project.customer
if not customer:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Project has no associated customer"
)
# Use the latest quote for this project to determine total price.
latest_quote = (
db.query(models.Quote)
.filter(models.Quote.project_id == project.id)
.order_by(models.Quote.created_at.desc())
.first()
)
if not latest_quote:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No quote found for this project.",
)
contracts_dir = Path("data/contracts")
contracts_dir.mkdir(parents=True, exist_ok=True)
output_path = contracts_dir / f"contract_project_{project.id}.docx"
template_path = Path("templates/contract_template.docx")
if not template_path.exists():
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Contract template file not found on server.",
)
# Build placeholder mapping
mapping: Dict[str, str] = {
"{{CUSTOMER_NAME}}": customer.name,
"{{TOTAL_PRICE}}": str(latest_quote.total_amount),
"{{DELIVERY_DATE}}": payload.delivery_date,
}
# Allow arbitrary additional placeholders
for key, value in payload.extra_placeholders.items():
mapping[key] = value
await generate_contract_word(mapping, str(template_path), str(output_path))
return ContractGenerateResponse(project_id=project.id, contract_path=str(output_path))