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,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 无需清理