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:
@@ -55,6 +55,9 @@ class FakeProjectRepo:
|
||||
theme=data.theme,
|
||||
selling_points=list(data.selling_points),
|
||||
structure=data.structure,
|
||||
tone=data.tone,
|
||||
ending_type=data.ending_type,
|
||||
narrative_pov=data.narrative_pov,
|
||||
updated_at=self.now,
|
||||
)
|
||||
self.rows[pid] = (owner_id, view)
|
||||
|
||||
@@ -150,6 +150,54 @@ async def test_create_project_returns_201() -> None:
|
||||
assert uuid.UUID(body["id"])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_project_persists_tone_ending_pov() -> None:
|
||||
# ④ 立项新增基调/结局/视角:请求 → repo 落库 → 响应回显(snake_case 契约)。
|
||||
client, repo, _, _ = _make_client()
|
||||
async with client:
|
||||
resp = await client.post(
|
||||
"/projects",
|
||||
json={
|
||||
"title": "基调作品",
|
||||
"tone": "热血",
|
||||
"ending_type": "HE",
|
||||
"narrative_pov": "第一人称",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["tone"] == "热血"
|
||||
assert body["ending_type"] == "HE"
|
||||
assert body["narrative_pov"] == "第一人称"
|
||||
(_owner, view) = next(iter(repo.rows.values()))
|
||||
assert view.tone == "热血"
|
||||
assert view.ending_type == "HE"
|
||||
assert view.narrative_pov == "第一人称"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_exposes_tone_ending_pov() -> None:
|
||||
client, _, _, _ = _make_client()
|
||||
async with client:
|
||||
created = (
|
||||
await client.post(
|
||||
"/projects",
|
||||
json={
|
||||
"title": "书",
|
||||
"tone": "沉重",
|
||||
"ending_type": "BE",
|
||||
"narrative_pov": "多视角",
|
||||
},
|
||||
)
|
||||
).json()
|
||||
resp = await client.get(f"/projects/{created['id']}")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["tone"] == "沉重"
|
||||
assert body["ending_type"] == "BE"
|
||||
assert body["narrative_pov"] == "多视角"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_projects() -> None:
|
||||
client, repo, _, _ = _make_client()
|
||||
|
||||
@@ -127,6 +127,9 @@ async def create_project(body: ProjectCreateRequest, repo: ProjectRepoDep) -> Pr
|
||||
theme=body.theme,
|
||||
selling_points=body.selling_points,
|
||||
structure=body.structure,
|
||||
tone=body.tone,
|
||||
ending_type=body.ending_type,
|
||||
narrative_pov=body.narrative_pov,
|
||||
),
|
||||
)
|
||||
log.info("project_created", project_id=str(view.id), title_len=len(view.title))
|
||||
|
||||
@@ -22,6 +22,10 @@ class ProjectCreateRequest(BaseModel):
|
||||
theme: str | None = None
|
||||
selling_points: list[str] = Field(default_factory=list)
|
||||
structure: str | None = None
|
||||
# 立项书级常量(灵感④)——free text + 前端预设,无 Literal(Literal 只留 Verdict)。
|
||||
tone: str | None = None
|
||||
ending_type: str | None = None
|
||||
narrative_pov: str | None = None
|
||||
|
||||
|
||||
class ProjectResponse(BaseModel):
|
||||
@@ -35,6 +39,9 @@ class ProjectResponse(BaseModel):
|
||||
theme: str | None = None
|
||||
selling_points: list[str] = Field(default_factory=list)
|
||||
structure: str | None = None
|
||||
tone: str | None = None
|
||||
ending_type: str | None = None
|
||||
narrative_pov: str | None = None
|
||||
updated_at: datetime | None = Field(default=None, description="项目最近更新时间")
|
||||
pending_review_count: int = Field(default=0, ge=0, description="待审稿章节数")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user