- 产品编辑入口统一走 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>
144 lines
6.2 KiB
Python
144 lines
6.2 KiB
Python
"""
|
||
app/models/product.py — products / benchmark_notes / banned_words
|
||
Alembic 003 业务主体(1/3)
|
||
products.category 是纯数据字段,禁止任何品类枚举(基石A)。
|
||
"""
|
||
|
||
from datetime import datetime
|
||
from typing import Optional
|
||
|
||
from sqlalchemy import (
|
||
BigInteger, DateTime, Enum, ForeignKey,
|
||
Index, Integer, String, Text, func,
|
||
)
|
||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||
|
||
from app.constants.enums import BannedWordLevel, ProductImageScene, ProductSource
|
||
from app.core.database import Base
|
||
|
||
|
||
class Product(Base):
|
||
"""产品档案(卖点/违禁词/风格/调性/文案角度/可调prompt/source)"""
|
||
__tablename__ = "products"
|
||
|
||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||
workspace_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
# category 是纯数据字段,不在代码里做枚举(基石A)
|
||
category: Mapped[str | None] = mapped_column(String(64))
|
||
source: Mapped[str] = mapped_column(
|
||
Enum(ProductSource, values_callable=lambda x: [e.value for e in x]),
|
||
default=ProductSource.CUSTOM, nullable=False,
|
||
)
|
||
selling_points: Mapped[str | None] = mapped_column(Text) # JSON数组
|
||
style_tone: Mapped[str | None] = mapped_column(String(128))
|
||
text_angles: Mapped[str | None] = mapped_column(Text) # JSON数组,用户设定
|
||
custom_prompt: Mapped[str | None] = mapped_column(Text) # 等北哥方案注入
|
||
image_path: Mapped[str | None] = mapped_column(String(512)) # 产品参考图路径(前端上传后写入)
|
||
# 008: 品牌词(客户输入,植入文案每条+生图特写图第2/6张,第5环)
|
||
brand_keyword: Mapped[str | None] = mapped_column(String(64), comment="品牌词,客户录入,随产品固定")
|
||
# 012: 目标人群(客户输入,透传进文案/生图 prompt,storyboard.py:52 原恒空现可填)
|
||
target_audience: Mapped[str | None] = mapped_column(String(128), comment="目标人群,客户输入,透传进文案/生图prompt")
|
||
is_active: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime, server_default=func.now(), nullable=False
|
||
)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
|
||
)
|
||
|
||
benchmark_notes: Mapped[list["BenchmarkNote"]] = relationship(
|
||
back_populates="product", lazy="noload"
|
||
)
|
||
images: Mapped[list["ProductImage"]] = relationship(
|
||
back_populates="product", lazy="selectin",
|
||
cascade="all, delete-orphan", order_by="ProductImage.sort_order",
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("idx_products_workspace_id", "workspace_id"),
|
||
)
|
||
|
||
|
||
class ProductImage(Base):
|
||
"""产品参考图(R5多图):一产品多张,每张标 scene 场景类型,生图按分镜role选用。
|
||
|
||
product.image_path 仍保留当主图(向后兼容,零破坏);本表是其超集。
|
||
"""
|
||
__tablename__ = "product_images"
|
||
|
||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||
product_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey("products.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
path: Mapped[str] = mapped_column(String(512), nullable=False) # 绝对路径,worker 直接 open
|
||
scene: Mapped[str] = mapped_column(
|
||
Enum(ProductImageScene, values_callable=lambda x: [e.value for e in x]),
|
||
default=ProductImageScene.PRIMARY, nullable=False,
|
||
)
|
||
is_primary: Mapped[bool] = mapped_column(default=False, nullable=False) # 主图(同步 product.image_path)
|
||
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime, server_default=func.now(), nullable=False
|
||
)
|
||
|
||
product: Mapped["Product"] = relationship(back_populates="images")
|
||
|
||
__table_args__ = (
|
||
Index("idx_product_images_product_id", "product_id"),
|
||
)
|
||
|
||
|
||
class BenchmarkNote(Base):
|
||
"""标杆笔记(截图+手填亮点为主通道)"""
|
||
__tablename__ = "benchmark_notes"
|
||
|
||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||
workspace_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
product_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey("products.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
screenshot_url: Mapped[str | None] = mapped_column(Text)
|
||
highlights: Mapped[str | None] = mapped_column(Text) # 手填亮点
|
||
link_url: Mapped[str | None] = mapped_column(Text)
|
||
# 009: 第2环标杆分析字段
|
||
features_json: Mapped[str | None] = mapped_column(Text, comment="爆款8特征分析结果(JSON),AI解析后写入")
|
||
analyze_status: Mapped[str] = mapped_column(String(20), default="pending", nullable=False, comment="AI分析状态: pending/analyzing/done/failed")
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime, server_default=func.now(), nullable=False
|
||
)
|
||
|
||
product: Mapped["Product"] = relationship(back_populates="benchmark_notes")
|
||
|
||
__table_args__ = (
|
||
Index("idx_benchmark_notes_product_id", "product_id"),
|
||
)
|
||
|
||
|
||
class BannedWord(Base):
|
||
"""违禁词库(三级:auto_fix/soft_warn/hard_block)"""
|
||
__tablename__ = "banned_words"
|
||
|
||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||
workspace_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
word: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
level: Mapped[str] = mapped_column(
|
||
Enum(BannedWordLevel, values_callable=lambda x: [e.value for e in x]),
|
||
nullable=False,
|
||
)
|
||
replacement: Mapped[str | None] = mapped_column(String(128))
|
||
updatable: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime, server_default=func.now(), nullable=False
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("idx_banned_words_workspace_id", "workspace_id"),
|
||
)
|