Files
beige/backend/app/models/product.py
yangqianqian df1856d793 上线版: 产品表单统一+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>
2026-06-30 18:08:13 +08:00

144 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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: 目标人群(客户输入,透传进文案/生图 promptstoryboard.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特征分析结果JSONAI解析后写入")
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"),
)