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:
yangqianqian
2026-06-18 17:32:49 +08:00
parent 285791c12f
commit 4bed7425a8
41 changed files with 1211 additions and 236 deletions

View 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