fix:优化数据
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from backend.app import models
|
||||
from backend.app.db import get_db
|
||||
@@ -11,25 +13,35 @@ from backend.app.schemas import (
|
||||
ContractGenerateResponse,
|
||||
ProjectRead,
|
||||
ProjectUpdate,
|
||||
PushToCloudRequest,
|
||||
PushToCloudResponse,
|
||||
QuoteGenerateResponse,
|
||||
RequirementAnalyzeRequest,
|
||||
RequirementAnalyzeResponse,
|
||||
)
|
||||
from backend.app.services.ai_service import analyze_requirement
|
||||
from backend.app.services.cloud_doc_service import CloudDocManager
|
||||
from backend.app.services.doc_service import (
|
||||
generate_contract_word,
|
||||
generate_quote_excel,
|
||||
generate_quote_pdf_from_data,
|
||||
)
|
||||
from backend.app.routers.cloud_doc_config import get_all_credentials
|
||||
|
||||
|
||||
router = APIRouter(prefix="/projects", tags=["projects"])
|
||||
|
||||
|
||||
def _build_markdown_from_analysis(data: Dict[str, Any]) -> str:
|
||||
def _build_markdown_from_analysis(data: Union[Dict[str, Any], List[Any]]) -> str:
|
||||
"""
|
||||
Convert structured AI analysis JSON into a human-editable Markdown document.
|
||||
Tolerates AI returning a list (e.g. modules only) and normalizes to a dict.
|
||||
"""
|
||||
if isinstance(data, list):
|
||||
data = {"modules": data, "total_estimated_hours": None, "total_amount": None, "notes": None}
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
|
||||
lines: list[str] = []
|
||||
lines.append("# 项目方案草稿")
|
||||
lines.append("")
|
||||
@@ -48,7 +60,15 @@ def _build_markdown_from_analysis(data: Dict[str, Any]) -> str:
|
||||
if modules:
|
||||
lines.append("## 功能模块与技术实现")
|
||||
for idx, module in enumerate(modules, start=1):
|
||||
name = module.get("name", f"模块 {idx}")
|
||||
if not isinstance(module, dict):
|
||||
# AI sometimes returns strings or other shapes; treat as a single title line
|
||||
raw_name = str(module).strip() if module else ""
|
||||
name = raw_name if len(raw_name) > 1 and raw_name not in (":", "[", "{", "}") else f"模块 {idx}"
|
||||
lines.append(f"### {idx}. {name}")
|
||||
lines.append("")
|
||||
continue
|
||||
raw_name = (module.get("name") or "").strip()
|
||||
name = raw_name if len(raw_name) > 1 and raw_name not in (":", "[", "{", "}") else f"模块 {idx}"
|
||||
desc = module.get("description") or ""
|
||||
tech = module.get("technical_approach") or ""
|
||||
hours = module.get("estimated_hours")
|
||||
@@ -83,13 +103,31 @@ def _build_markdown_from_analysis(data: Dict[str, Any]) -> str:
|
||||
|
||||
|
||||
@router.get("/", response_model=list[ProjectRead])
|
||||
async def list_projects(db: Session = Depends(get_db)):
|
||||
projects = (
|
||||
async def list_projects(
|
||||
customer_tag: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""列表项目;customer_tag 不为空时只返回该客户标签下的项目(按客户 tags 筛选)。"""
|
||||
query = (
|
||||
db.query(models.Project)
|
||||
.options(joinedload(models.Project.customer))
|
||||
.join(models.Customer)
|
||||
.order_by(models.Project.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
return projects
|
||||
if customer_tag and customer_tag.strip():
|
||||
tag = customer_tag.strip()
|
||||
# 客户 tags 逗号分隔,按整词匹配
|
||||
from sqlalchemy import or_
|
||||
t = models.Customer.tags
|
||||
query = query.filter(
|
||||
or_(
|
||||
t == tag,
|
||||
t.ilike(f"{tag},%"),
|
||||
t.ilike(f"%,{tag},%"),
|
||||
t.ilike(f"%,{tag}"),
|
||||
)
|
||||
)
|
||||
return query.all()
|
||||
|
||||
|
||||
@router.get("/{project_id}", response_model=ProjectRead)
|
||||
@@ -109,6 +147,8 @@ async def update_project(
|
||||
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.raw_requirement is not None:
|
||||
project.raw_requirement = payload.raw_requirement
|
||||
if payload.ai_solution_md is not None:
|
||||
project.ai_solution_md = payload.ai_solution_md
|
||||
if payload.status is not None:
|
||||
@@ -123,12 +163,24 @@ async def analyze_project_requirement(
|
||||
payload: RequirementAnalyzeRequest,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
logging.getLogger(__name__).info(
|
||||
"收到 AI 解析请求: customer_id=%s, 需求长度=%d",
|
||||
payload.customer_id,
|
||||
len(payload.raw_text or ""),
|
||||
)
|
||||
# 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)
|
||||
try:
|
||||
analysis = await analyze_requirement(payload.raw_text)
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e),
|
||||
) from e
|
||||
|
||||
ai_solution_md = _build_markdown_from_analysis(analysis)
|
||||
|
||||
project = models.Project(
|
||||
@@ -151,6 +203,7 @@ async def analyze_project_requirement(
|
||||
@router.post("/{project_id}/generate_quote", response_model=QuoteGenerateResponse)
|
||||
async def generate_project_quote(
|
||||
project_id: int,
|
||||
template: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
project = db.query(models.Project).get(project_id)
|
||||
@@ -167,7 +220,9 @@ async def generate_project_quote(
|
||||
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")
|
||||
from backend.app.routers.settings import get_quote_template_path
|
||||
|
||||
template_path = get_quote_template_path(template)
|
||||
if not template_path.exists():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -254,3 +309,61 @@ async def generate_project_contract(
|
||||
|
||||
return ContractGenerateResponse(project_id=project.id, contract_path=str(output_path))
|
||||
|
||||
|
||||
@router.post("/{project_id}/push-to-cloud", response_model=PushToCloudResponse)
|
||||
async def push_project_to_cloud(
|
||||
project_id: int,
|
||||
payload: PushToCloudRequest,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
将当前项目方案(Markdown)推送到云文档。若该项目此前已推送过该平台,则更新原文档(增量同步)。
|
||||
"""
|
||||
project = db.query(models.Project).options(joinedload(models.Project.customer)).get(project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
platform = (payload.platform or "").strip().lower()
|
||||
if platform not in ("feishu", "yuque", "tencent"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="platform 须为 feishu / yuque / tencent",
|
||||
)
|
||||
title = (payload.title or "").strip() or f"项目方案 - 项目#{project_id}"
|
||||
body_md = (payload.body_md if payload.body_md is not None else project.ai_solution_md) or ""
|
||||
if not body_md.strip():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="暂无方案内容,请先在编辑器中填写或保存方案后再推送",
|
||||
)
|
||||
existing = (
|
||||
db.query(models.ProjectCloudDoc)
|
||||
.filter(
|
||||
models.ProjectCloudDoc.project_id == project_id,
|
||||
models.ProjectCloudDoc.platform == platform,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
existing_doc_id = existing.cloud_doc_id if existing else None
|
||||
credentials = get_all_credentials()
|
||||
manager = CloudDocManager(credentials)
|
||||
try:
|
||||
cloud_doc_id, url = await manager.push_markdown(
|
||||
platform, title, body_md, existing_doc_id=existing_doc_id
|
||||
)
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
|
||||
if existing:
|
||||
existing.cloud_doc_id = cloud_doc_id
|
||||
existing.cloud_url = url
|
||||
existing.updated_at = datetime.now(timezone.utc)
|
||||
else:
|
||||
record = models.ProjectCloudDoc(
|
||||
project_id=project_id,
|
||||
platform=platform,
|
||||
cloud_doc_id=cloud_doc_id,
|
||||
cloud_url=url,
|
||||
)
|
||||
db.add(record)
|
||||
db.commit()
|
||||
return PushToCloudResponse(url=url, cloud_doc_id=cloud_doc_id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user