fix:优化数据

This commit is contained in:
丹尼尔
2026-03-15 16:38:59 +08:00
parent a609f81a36
commit 3aa1a586e5
43 changed files with 14565 additions and 294 deletions

View File

@@ -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)