A8多套打包+M4归档+R5多图:存量功能备份
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>
This commit is contained in:
100
backend/app/api/v1/exports.py
Normal file
100
backend/app/api/v1/exports.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user