上线版: 产品表单统一+form嵌套修复+用户管理+部署+三套叙事
- 产品编辑入口统一走 ProductFormFull(卖点/风格/人群/品牌词全字段); 修复开任务页 <form> 套 <form> 致"编辑产品"报错、改不了、跳回首个产品 - dashboard 入口卡片对齐实际路由: 系统管理(/config) 与 工作配置(/settings) 分开; settings ?tab=products 直达改用挂载后读 URL, 消除 hydration mismatch - 新增用户管理(users API/admin service/改密页) + alembic 022/023/024 - 上线部署: Dockerfile / docker-compose.prod+https / nginx https / .env.example - A8 三套正交叙事(痛点/场景/成分背书) + beige 调色去AI化 + 飞轮 text_import 高权重信号 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ from app.core.database import get_db
|
||||
from app.core.response import ok, paginate, raise_not_found
|
||||
from app.middleware.workspace_guard import CurrentUser, require_admin, require_write_permission
|
||||
from app.models.product import BannedWord, BenchmarkNote, Product, ProductImage
|
||||
from app.models.task import GenerationTask
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=["products"])
|
||||
@@ -121,6 +122,28 @@ def get_product(
|
||||
return ok(_fmt_product(p))
|
||||
|
||||
|
||||
@router.get("/products/{product_id}/preference/context")
|
||||
def get_product_preference_context(
|
||||
product_id: int,
|
||||
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""产品维度飞轮偏好上下文(新建任务页用,此时还没有 task_id)。
|
||||
与 task 维度端点同口径,复用 flywheel_service.get_preference_context。"""
|
||||
p = db.query(Product).filter(
|
||||
Product.id == product_id, Product.workspace_id == current_user.workspace_id
|
||||
).first()
|
||||
if not p:
|
||||
raise_not_found("产品不存在")
|
||||
from app.services.flywheel_service import get_preference_context
|
||||
product_dict = {
|
||||
"custom_prompt": p.custom_prompt or "",
|
||||
"style_tone": p.style_tone or "",
|
||||
}
|
||||
ctx = get_preference_context(db, current_user.workspace_id, product_id, product_dict)
|
||||
return ok(ctx)
|
||||
|
||||
|
||||
@router.put("/products/{product_id}")
|
||||
def update_product(
|
||||
product_id: int, body: ProductCreate,
|
||||
@@ -148,12 +171,21 @@ def delete_product(
|
||||
current_user: Annotated[CurrentUser, Depends(require_admin)] = None,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""智能删除(仅管理员):没跑过任务的产品物理删干净(级联清图片/标杆),
|
||||
已有历史任务的产品改软删保留历史,避免 generation_tasks 外键报错。"""
|
||||
p = db.query(Product).filter(Product.id == product_id, Product.workspace_id == current_user.workspace_id).first()
|
||||
if not p:
|
||||
raise_not_found("产品不存在")
|
||||
p.is_active = False # 软删
|
||||
has_task = db.query(GenerationTask.id).filter(GenerationTask.product_id == product_id).first() is not None
|
||||
if has_task:
|
||||
p.is_active = False # 有历史任务→软删保留
|
||||
db.commit()
|
||||
return ok({"deleted": product_id, "mode": "soft", "reason": "产品已有历史任务,已停用并保留记录"})
|
||||
db.query(ProductImage).filter(ProductImage.product_id == product_id).delete()
|
||||
db.query(BenchmarkNote).filter(BenchmarkNote.product_id == product_id).delete()
|
||||
db.delete(p) # 无任务→物理删(已先显式清图片/标杆,不赌数据库级联)
|
||||
db.commit()
|
||||
return ok({"deleted": product_id})
|
||||
return ok({"deleted": product_id, "mode": "hard"})
|
||||
|
||||
|
||||
# ── 产品参考图上传 ──────────────────────────────────────────
|
||||
@@ -337,8 +369,17 @@ async def analyze_product_image(
|
||||
raise_business("未配置 API Key,请先在设置中录入")
|
||||
plain_key = decrypt_key(api_key_row.encrypted_key)
|
||||
|
||||
clients = build_ai_clients(plain_key)
|
||||
# codeproxy 备用 key(可选,用户录入则用,没录回落 env;不抛错)
|
||||
alt_row = db.query(UserApiKey).filter(
|
||||
UserApiKey.user_id == current_user.user_id,
|
||||
UserApiKey.workspace_id == current_user.workspace_id,
|
||||
UserApiKey.provider == "codeproxy",
|
||||
).first()
|
||||
alt_key = decrypt_key(alt_row.encrypted_key) if alt_row else None
|
||||
|
||||
clients = build_ai_clients(plain_key, alt_key=alt_key)
|
||||
plain_key = None # 立即清零,不传出(基石B)
|
||||
alt_key = None
|
||||
|
||||
try:
|
||||
raw = await clients.gpt_vision_analyze(_VISION_PROMPT, [data])
|
||||
|
||||
Reference in New Issue
Block a user