A8 多套交付包(packaging_task.py): - 修复交付包只打第1条混乱note的bug,按ImageCandidate.strategy分A/B/C组 - 每组生独立note_0N夹(6图+文案.txt),同seq留最新去重,老数据兼容 - task74端到端验:3套各6图,独立agent7项交叉验证全过 M4 归档(tasks.py/exports.py/前端): - list_tasks加date_from/date_to/product_id筛选+product_name批量填(防N+1) - 新增exports.py:产品JSON导出+标杆CSV导出(UTF-8 BOM) - 前端HistoryFilters日期/产品筛选+产品列+打回原因红banner - response.py加raise_param_error;独立agent验A1/A2/A9通过 R5 产品多图(product_images.py/020迁移/前端): - product_images表+5端点(上传/列/改场景/设主图/删图) - 生图按ROLE_SCENE_PREFERENCE选对应场景图,回落primary - 前端ProductImageManager多图画廊 R6 账号config拆页(settings/): - 配置页按角色拆/settings(运营+组长+admin)+/config(仅admin) - Key只显末4位不显余额(守红线) 核销表对齐真实代码状态:D1改稿框/M7裂变/E12评图分纠偏为已完成(曾漏回写) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
"""
|
||
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
|