""" app/api/v1/exports.py — 客户原始输入物导出(数据归属红线) 产品/标杆属客户 client_data,可导出(JSON/CSV);飞轮偏好属平台不在此列。 require_write_permission:仅 workspace 成员可导出本工作区数据。 """ import csv import io import json import logging from typing import Annotated from fastapi import APIRouter, Depends, Query from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from app.core.database import get_db from app.middleware.workspace_guard import CurrentUser, require_write_permission from app.models.product import BenchmarkNote, Product logger = logging.getLogger(__name__) router = APIRouter(tags=["exports"]) def _csv_response(rows: list[dict], fieldnames: list[str], filename: str) -> StreamingResponse: """通用 CSV 流式响应。加 UTF-8 BOM 防 Windows Excel 中文乱码。""" buf = io.StringIO() buf.write("") # BOM writer = csv.DictWriter(buf, fieldnames=fieldnames, extrasaction="ignore") writer.writeheader() for r in rows: writer.writerow(r) buf.seek(0) return StreamingResponse( iter([buf.getvalue()]), media_type="text/csv; charset=utf-8", headers={"Content-Disposition": f'attachment; filename="{filename}"'}, ) @router.get("/products/export") def export_products( current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None, db: Session = Depends(get_db), ): """产品档案导出(JSON)。selling_points 还原为数组,非 JSON 字符串。""" rows = ( db.query(Product) .filter(Product.workspace_id == current_user.workspace_id, Product.is_active == True) .order_by(Product.id) .all() ) data = [ { "id": p.id, "name": p.name, "category": p.category, "selling_points": json.loads(p.selling_points) if p.selling_points else [], "brand_keyword": p.brand_keyword, "target_audience": p.target_audience, "style_tone": p.style_tone, "created_at": p.created_at.isoformat(), } for p in rows ] # 直接返数组(非包 ok 信封):导出供下载/二次处理,前端按 blob 存盘。 return data @router.get("/benchmarks/export") def export_benchmarks( format: str = Query(default="json", pattern="^(json|csv)$"), current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None, db: Session = Depends(get_db), ): """标杆笔记导出(JSON/CSV)。JOIN Product 填 product_name 便于客户阅读。""" rows = ( db.query(BenchmarkNote, Product.name) .outerjoin(Product, BenchmarkNote.product_id == Product.id) .filter(BenchmarkNote.workspace_id == current_user.workspace_id) .order_by(BenchmarkNote.id) .all() ) data = [ { "id": b.id, "product_id": b.product_id, "product_name": pname, "highlights": b.highlights or "", "link_url": b.link_url or "", "analyze_status": b.analyze_status, "created_at": b.created_at.isoformat(), } for b, pname in rows ] if format == "csv": fields = ["id", "product_id", "product_name", "highlights", "link_url", "analyze_status", "created_at"] return _csv_response(data, fields, "benchmarks.csv") return data