feat(web): 立项新增 基调/结局/视角 三字段(书级常量入 stable_core 缓存前缀)

灵感计划 §3④ + §4 D 枚举决策:
- projects 表加 tone/ending_type/narrative_pov(Text nullable、无 CHECK,同 genre/structure;
  迁移 e5f6a7b8c9d0 nullable 免 backfill)。
- ProjectCreateRequest/ProjectResponse + domain ProjectCreate/ProjectView 全链路透传(snake_case)。
- 完整 stable_core 链路:ProjectSpecView + SqlProjectSpecRepo.spec + _build_spec_section 渲染进
  「作品蓝本」块——书级常量随书稳定,字节稳定守缓存前缀不变量 #9,绝不入 volatile。
- 前端 wizard.ts 三字段 + 预设数组 TONES/ENDING_TYPES/NARRATIVE_POVS + ProjectWizard 控件与确认页回显。
- 契约变更已 pnpm gen:api + memory/contracts.md 登记。
This commit is contained in:
Yaojia Wang
2026-07-06 15:59:49 +02:00
parent 491d9342ef
commit 821ace5989
17 changed files with 247 additions and 1 deletions

View File

@@ -0,0 +1,33 @@
"""Add tone/ending_type/narrative_pov to projects (灵感④ 立项书级常量).
三列均 Text nullable与既有 genre/structure 一致,无 CHECK枚举约束留前端预设
nullable 免 backfill旧项目留 NULLassemble/前端优雅降级)。
Revision ID: e5f6a7b8c9d0
Revises: 8c1d2e3f4a5b
Create Date: 2026-07-06 00:00:00.000000
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "e5f6a7b8c9d0"
down_revision: str | None = "8c1d2e3f4a5b"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.add_column("projects", sa.Column("tone", sa.Text(), nullable=True))
op.add_column("projects", sa.Column("ending_type", sa.Text(), nullable=True))
op.add_column("projects", sa.Column("narrative_pov", sa.Text(), nullable=True))
def downgrade() -> None:
op.drop_column("projects", "narrative_pov")
op.drop_column("projects", "ending_type")
op.drop_column("projects", "tone")

View File

@@ -48,6 +48,10 @@ class Project(UuidPk, TimestampedMixin, Base):
theme: Mapped[str | None] = mapped_column(Text)
selling_points: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
structure: Mapped[str | None] = mapped_column(Text)
# 立项书级常量(灵感④)——同 genre/structureText nullable、无 CHECK枚举约束留前端预设
tone: Mapped[str | None] = mapped_column(Text)
ending_type: Mapped[str | None] = mapped_column(Text)
narrative_pov: Mapped[str | None] = mapped_column(Text)
class Character(UuidPk, CreatedAt, Base):