上线版: 产品表单统一+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:
yangqianqian
2026-06-30 18:08:13 +08:00
parent a77212781c
commit df1856d793
150 changed files with 8616 additions and 1765 deletions

View File

@@ -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])