baseline: Clover 独立仓库首次基线提交

将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。
当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包),
M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-16 11:30:22 +08:00
commit 6a2632da70
253 changed files with 27467 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
"""001_banana_users_tables
Revision ID: 001
Revises:
Create Date: 2026-06-09
Alembic 001: 从 banana 搬 3 张表users/login_records/user_preferences
删除 credits 字段,其余无改。
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "001"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"users",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("username", sa.String(64), nullable=False),
sa.Column("email", sa.String(255), nullable=False),
sa.Column("hashed_password", sa.String(255), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="1"),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("username"),
sa.UniqueConstraint("email"),
)
op.create_table(
"login_records",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=False),
sa.Column("ip_address", sa.String(64), nullable=True),
sa.Column("user_agent", sa.String(512), nullable=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("idx_login_records_user_id", "login_records", ["user_id"])
op.create_table(
"user_preferences",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=False),
sa.Column("preferences_json", sa.Text(), nullable=True),
sa.Column("updated_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user_id"),
)
def downgrade() -> None:
op.drop_table("user_preferences")
op.drop_index("idx_login_records_user_id", "login_records")
op.drop_table("login_records")
op.drop_table("users")

View File

@@ -0,0 +1,71 @@
"""002_multitenant_workspace
Revision ID: 002
Revises: 001
Create Date: 2026-06-09
Alembic 002: 多租户基础workspaces/workspace_members/user_api_keys
matrix_accounts 二期预留,一期不建。
user_api_keys 不存 url 字段token站固定自家站基石B
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "002"
down_revision: Union[str, None] = "001"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"workspaces",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("name", sa.String(128), nullable=False),
sa.Column("slug", sa.String(64), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="1"),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("slug"),
)
op.create_table(
"workspace_members",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=False),
sa.Column("role", sa.Enum("admin", "supervisor", "operator", name="userrole"), nullable=False),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("workspace_id", "user_id", name="uq_workspace_member"),
)
op.create_index("idx_workspace_members_workspace_id", "workspace_members", ["workspace_id"])
op.create_table(
"user_api_keys",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=False),
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
sa.Column("provider", sa.String(32), nullable=False),
sa.Column("encrypted_key", sa.String(512), nullable=False),
sa.Column("key_last4", sa.String(4), nullable=False),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user_id", "workspace_id", "provider", name="uq_user_workspace_provider"),
)
op.create_index("idx_user_api_keys_workspace_id", "user_api_keys", ["workspace_id"])
def downgrade() -> None:
op.drop_index("idx_user_api_keys_workspace_id", "user_api_keys")
op.drop_table("user_api_keys")
op.drop_index("idx_workspace_members_workspace_id", "workspace_members")
op.drop_table("workspace_members")
op.drop_table("workspaces")
op.execute("DROP TYPE IF EXISTS userrole")

View File

@@ -0,0 +1,205 @@
"""003_business_tables
Revision ID: 003
Revises: 002
Create Date: 2026-06-09
Alembic 003: 业务主体7张表 + ai_call_logs
products / benchmark_notes / banned_words /
generation_tasks / text_candidates / image_candidates /
delivery_packages / ai_call_logs
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "003"
down_revision: Union[str, None] = "002"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
_TASK_STATUS = sa.Enum(
"pending", "generating", "pending_selection",
"pending_review", "approved", "rejected", "archived",
name="taskstatus",
)
_REVIEW_STATUS = sa.Enum("pending", "approved", "rejected", name="reviewstatus")
_CANDIDATE_SOURCE = sa.Enum("ai", "import", name="candidatesource")
_BANNED_LEVEL = sa.Enum("auto_fix", "soft_warn", "hard_block", name="bannedwordlevel")
_BANNED_STATUS = sa.Enum("pass", "auto_fixed", "soft_warn", "hard_block", name="bannedwordstatus")
_IMAGE_ROLE = sa.Enum("hook", "pain", "proof", "quality", "credit", "convert", "main", name="imagerole")
_PKG_STATUS = sa.Enum("pending", "ready", "downloaded", name="packagestatus")
_PRODUCT_SOURCE = sa.Enum("preset", "custom", name="productsource")
def upgrade() -> None:
op.create_table(
"products",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
sa.Column("name", sa.String(128), nullable=False),
sa.Column("category", sa.String(64), nullable=True),
sa.Column("source", _PRODUCT_SOURCE, nullable=False, server_default="custom"),
sa.Column("selling_points", sa.Text(), nullable=True),
sa.Column("style_tone", sa.String(128), nullable=True),
sa.Column("text_angles", sa.Text(), nullable=True),
sa.Column("custom_prompt", sa.Text(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="1"),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("idx_products_workspace_id", "products", ["workspace_id"])
op.create_table(
"benchmark_notes",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
sa.Column("product_id", sa.BigInteger(), nullable=False),
sa.Column("screenshot_url", sa.String(512), nullable=True),
sa.Column("highlights", sa.Text(), nullable=True),
sa.Column("link_url", sa.String(512), nullable=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["product_id"], ["products.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("idx_benchmark_notes_product_id", "benchmark_notes", ["product_id"])
op.create_table(
"banned_words",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
sa.Column("word", sa.String(64), nullable=False),
sa.Column("level", _BANNED_LEVEL, nullable=False),
sa.Column("replacement", sa.String(128), nullable=True),
sa.Column("updatable", sa.Boolean(), nullable=False, server_default="1"),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("idx_banned_words_workspace_id", "banned_words", ["workspace_id"])
op.create_table(
"generation_tasks",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
sa.Column("product_id", sa.BigInteger(), nullable=False),
sa.Column("operator_id", sa.BigInteger(), nullable=False),
sa.Column("theme", sa.String(256), nullable=True),
sa.Column("text_count", sa.Integer(), nullable=False, server_default="5"),
sa.Column("image_count", sa.Integer(), nullable=False, server_default="3"),
sa.Column("track", sa.String(16), nullable=False, server_default="ai"),
sa.Column("status", _TASK_STATUS, nullable=False, server_default="pending"),
sa.Column("mongo_trace_id", sa.String(24), nullable=True),
sa.Column("review_status", _REVIEW_STATUS, nullable=True),
sa.Column("reviewer_id", sa.BigInteger(), nullable=True),
sa.Column("reviewed_at", sa.DateTime(), nullable=True),
sa.Column("reject_reason", sa.Text(), nullable=True),
sa.Column("approved_at", sa.DateTime(), nullable=True),
sa.Column("archived_at", sa.DateTime(), nullable=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["product_id"], ["products.id"]),
sa.ForeignKeyConstraint(["operator_id"], ["users.id"]),
sa.ForeignKeyConstraint(["reviewer_id"], ["users.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("idx_generation_tasks_workspace_status", "generation_tasks", ["workspace_id", "status"])
op.create_table(
"text_candidates",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
sa.Column("task_id", sa.BigInteger(), nullable=False),
sa.Column("source", _CANDIDATE_SOURCE, nullable=False, server_default="ai"),
sa.Column("angle_label", sa.String(64), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("score_json", sa.Text(), nullable=True),
sa.Column("banned_word_status", _BANNED_STATUS, nullable=False, server_default="pass"),
sa.Column("eval_score", sa.Float(), nullable=True),
sa.Column("is_selected", sa.Boolean(), nullable=False, server_default="0"),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["task_id"], ["generation_tasks.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("idx_text_candidates_task_id", "text_candidates", ["task_id"])
op.create_table(
"image_candidates",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
sa.Column("task_id", sa.BigInteger(), nullable=False),
sa.Column("role", _IMAGE_ROLE, nullable=False, server_default="main"),
sa.Column("url", sa.String(512), nullable=True),
sa.Column("strategy", sa.String(4), nullable=True),
sa.Column("seq", sa.Integer(), nullable=False, server_default="1"),
sa.Column("is_selected", sa.Boolean(), nullable=False, server_default="0"),
sa.Column("eval_score", sa.Float(), nullable=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["task_id"], ["generation_tasks.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("idx_image_candidates_task_id", "image_candidates", ["task_id"])
op.create_table(
"delivery_packages",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
sa.Column("task_id", sa.BigInteger(), nullable=False),
sa.Column("status", _PKG_STATUS, nullable=False, server_default="pending"),
sa.Column("package_path", sa.String(512), nullable=True),
sa.Column("download_url", sa.String(512), nullable=True),
sa.Column("expires_at", sa.DateTime(), nullable=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["task_id"], ["generation_tasks.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"ai_call_logs",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=False),
sa.Column("key_id", sa.BigInteger(), nullable=True),
sa.Column("task_id", sa.BigInteger(), nullable=True),
sa.Column("provider", sa.String(32), nullable=True),
sa.Column("model", sa.String(64), nullable=True),
sa.Column("call_type", sa.String(32), nullable=True),
sa.Column("prompt_tokens", sa.Integer(), nullable=True),
sa.Column("completion_tokens", sa.Integer(), nullable=True),
sa.Column("success", sa.Boolean(), nullable=False, server_default="1"),
sa.Column("error_code", sa.String(32), nullable=True),
sa.Column("latency_ms", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
sa.ForeignKeyConstraint(["key_id"], ["user_api_keys.id"]),
sa.ForeignKeyConstraint(["task_id"], ["generation_tasks.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("idx_ai_call_logs_workspace_user", "ai_call_logs", ["workspace_id", "user_id"])
op.create_index("idx_ai_call_logs_task_id", "ai_call_logs", ["task_id"])
def downgrade() -> None:
op.drop_index("idx_ai_call_logs_task_id", "ai_call_logs")
op.drop_index("idx_ai_call_logs_workspace_user", "ai_call_logs")
op.drop_table("ai_call_logs")
op.drop_table("delivery_packages")
op.drop_index("idx_image_candidates_task_id", "image_candidates")
op.drop_table("image_candidates")
op.drop_index("idx_text_candidates_task_id", "text_candidates")
op.drop_table("text_candidates")
op.drop_index("idx_generation_tasks_workspace_status", "generation_tasks")
op.drop_table("generation_tasks")
op.drop_index("idx_banned_words_workspace_id", "banned_words")
op.drop_table("banned_words")
op.drop_index("idx_benchmark_notes_product_id", "benchmark_notes")
op.drop_table("benchmark_notes")
op.drop_index("idx_products_workspace_id", "products")
op.drop_table("products")
# MySQL 不支持 DROP TYPE枚举存 VARCHAR 无需清理

View File

@@ -0,0 +1,67 @@
"""004_flywheel_preference_events
Revision ID: 004
Revises: 003
Create Date: 2026-06-09
Alembic 004: 飞轮信号表
preference_events 一期建表写入
preference_profile 二期预留,一期不建
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "004"
down_revision: Union[str, None] = "003"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
_SIGNAL_TYPE = sa.Enum(
"text_select", "image_select", "approve",
"reject_with_reason", "regenerate",
name="signaltype",
)
_DATA_OWNERSHIP = sa.Enum("client_data", "platform_asset", name="dataownership")
def upgrade() -> None:
op.create_table(
"preference_events",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
sa.Column("product_id", sa.BigInteger(), nullable=False),
sa.Column("task_id", sa.BigInteger(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=False),
sa.Column("signal_type", _SIGNAL_TYPE, nullable=False),
sa.Column("signal_weight", sa.Integer(), nullable=False),
sa.Column("candidate_id", sa.BigInteger(), nullable=True),
sa.Column("angle_label", sa.String(64), nullable=True),
sa.Column("reason", sa.Text(), nullable=True),
sa.Column("signal_meta", sa.Text(), nullable=True),
sa.Column(
"data_ownership", _DATA_OWNERSHIP,
nullable=False, server_default="client_data",
),
sa.Column(
"created_at", sa.DateTime(),
server_default=sa.text("now()"), nullable=False,
),
sa.ForeignKeyConstraint(["product_id"], ["products.id"]),
sa.ForeignKeyConstraint(["task_id"], ["generation_tasks.id"]),
sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"idx_preference_events_ws_product_created",
"preference_events",
["workspace_id", "product_id", "created_at"],
)
# preference_profile 二期预留,一期不建
def downgrade() -> None:
op.drop_index("idx_preference_events_ws_product_created", "preference_events")
op.drop_table("preference_events")
# MySQL 不支持 DROP TYPE枚举存 VARCHAR 无需清理

View File

@@ -0,0 +1,27 @@
"""005_product_image_path — products 表加 image_path 列(产品参考图本地路径)
前端图片上传完成后BE 接口写入此字段pipeline_io 读取用于生图参考。
"""
from alembic import op
import sqlalchemy as sa
revision = "005"
down_revision = "004"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"products",
sa.Column(
"image_path",
sa.String(512),
nullable=True,
comment="产品参考图本地文件路径(前端上传后写入,生图时作 reference_images 注入)",
),
)
def downgrade() -> None:
op.drop_column("products", "image_path")

View File

@@ -0,0 +1,45 @@
"""006_image_role_varchar — image_candidates.role 从 enum 改 varchar(32)
根因storyboard 角色名仍在演进hook/product_closeup/ingredient/texture/
applied_proof/closer/pain_scene/social_proof/scenario/tutorial…固定 enum
每加一个新角色就要迁一次,且旧 enum 漏了 product_closeup/ingredient 直接导致
落库 "Data truncated for column 'role'" → 生图任务无限重试卡死。
改 varchar 后落库不再受 enum 约束,角色名由 ImageRole 枚举在应用层定义。
"""
from alembic import op
import sqlalchemy as sa
revision = "006"
down_revision = "005"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.alter_column(
"image_candidates",
"role",
existing_type=sa.Enum(
"hook", "pain", "proof", "quality", "credit", "convert", "main",
name="role",
),
type_=sa.String(32),
existing_nullable=False,
server_default="hook",
comment="分镜角色名storyboard 输出,应用层 ImageRole 定义,不在 DB 约束)",
)
def downgrade() -> None:
op.alter_column(
"image_candidates",
"role",
existing_type=sa.String(32),
type_=sa.Enum(
"hook", "pain", "proof", "quality", "credit", "convert", "main",
name="role",
),
existing_nullable=False,
server_default="main",
)

View File

@@ -0,0 +1,33 @@
"""add need_product_image to generation_tasks
Revision ID: 007
Revises: 006
Create Date: 2026-06-08
本次产品是否入镜开关True=必须产品参考图(无图禁生成,不降级)False=允许纯文生图。
默认 True铁律生图优先带产品参考图
"""
from alembic import op
import sqlalchemy as sa
revision = "007"
down_revision = "006"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"generation_tasks",
sa.Column(
"need_product_image",
sa.Boolean(),
nullable=False,
server_default=sa.true(),
comment="本次产品是否入镜:True需产品图(无图禁生成不降级)/False允许纯文生图",
),
)
def downgrade():
op.drop_column("generation_tasks", "need_product_image")

View File

@@ -0,0 +1,32 @@
"""add brand_keyword to products
Revision ID: 008
Revises: 007
Create Date: 2026-06-13
第5环品牌词字段客户输入植入文案每条+生图特写图(第2/6张)。
brand_keyword 是产品级字段随产品固定由客户录入非AI生成
"""
from alembic import op
import sqlalchemy as sa
revision = "008"
down_revision = "007"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"products",
sa.Column(
"brand_keyword",
sa.String(64),
nullable=True,
comment="品牌词(客户输入,植入文案每条+生图特写图第2/6张",
),
)
def downgrade():
op.drop_column("products", "brand_keyword")

View File

@@ -0,0 +1,44 @@
"""add features_json and analyze_status to benchmark_notes
Revision ID: 009
Revises: 008
Create Date: 2026-06-13
第2环标杆分析字段
- features_json: 存储8特征结构化分析结果TEXT JSON
- analyze_status: AI分析状态机 pending/analyzing/done/failed
"""
from alembic import op
import sqlalchemy as sa
revision = "009"
down_revision = "008"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"benchmark_notes",
sa.Column(
"features_json",
sa.Text,
nullable=True,
comment="爆款8特征分析结果JSON第2环AI解析后写入",
),
)
op.add_column(
"benchmark_notes",
sa.Column(
"analyze_status",
sa.String(20),
nullable=False,
server_default="pending",
comment="AI分析状态: pending/analyzing/done/failed",
),
)
def downgrade():
op.drop_column("benchmark_notes", "analyze_status")
op.drop_column("benchmark_notes", "features_json")

View File

@@ -0,0 +1,70 @@
"""create fission_tasks table
Revision ID: 010
Revises: 009
Create Date: 2026-06-13
第11环裂变引擎1爆款→N套完整笔记包非只换文案
fission_tasks 是裂变任务主表,挂载在工作台内呈现。
reference_level: low/mid/high参考程度
"""
from alembic import op
import sqlalchemy as sa
revision = "010"
down_revision = "009"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"fission_tasks",
sa.Column("id", sa.BigInteger, primary_key=True, autoincrement=True),
sa.Column(
"workspace_id",
sa.BigInteger,
sa.ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False,
comment="多租户隔离",
),
sa.Column(
"source_note",
sa.Text,
nullable=True,
comment="爆款源笔记内容(文案+图描述JSON存储",
),
sa.Column(
"reference_level",
sa.String(10),
nullable=False,
server_default="mid",
comment="参考程度: low/mid/high",
),
sa.Column(
"fanout_count",
sa.Integer,
nullable=False,
server_default="3",
comment="裂变套数默认3套",
),
sa.Column(
"status",
sa.String(20),
nullable=False,
server_default="pending",
comment="状态: pending/generating/done/failed",
),
sa.Column(
"created_at",
sa.DateTime,
server_default=sa.func.now(),
nullable=False,
),
)
op.create_index("idx_fission_tasks_workspace_id", "fission_tasks", ["workspace_id"])
def downgrade():
op.drop_index("idx_fission_tasks_workspace_id", table_name="fission_tasks")
op.drop_table("fission_tasks")

View File

@@ -0,0 +1,43 @@
"""add benchmark_ids and source_fission_id to generation_tasks
Revision ID: 011
Revises: 010
Create Date: 2026-06-13
合并第2环S12+第11环两个需求
- benchmark_ids: 关联标杆笔记ID列表TEXT存JSON list第2环标杆分析引用
- source_fission_id: 裂变来源任务IDNULL=普通任务非NULL=裂变产出第11环
"""
from alembic import op
import sqlalchemy as sa
revision = "011"
down_revision = "010"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"generation_tasks",
sa.Column(
"benchmark_ids",
sa.Text,
nullable=True,
comment="关联标杆笔记ID列表JSON list第2环标杆分析引用",
),
)
op.add_column(
"generation_tasks",
sa.Column(
"source_fission_id",
sa.Integer,
nullable=True,
comment="裂变来源fission_task IDNULL=普通任务非NULL=裂变产出第11环",
),
)
def downgrade():
op.drop_column("generation_tasks", "source_fission_id")
op.drop_column("generation_tasks", "benchmark_ids")

View File

@@ -0,0 +1,29 @@
"""012 products 表加 target_audience 字段
Revision ID: 012_product_target_audience
Revises: 011_task_benchmark_fission_fields
Create Date: 2026-06-15
"""
from alembic import op
import sqlalchemy as sa
revision = "012"
down_revision = "011"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"products",
sa.Column(
"target_audience",
sa.String(128),
nullable=True,
comment="目标人群,客户输入,透传进文案/生图prompt",
),
)
def downgrade():
op.drop_column("products", "target_audience")