chore(prep): shared groundwork for batch-4 (report pipeline)

第四批(T15 编排/T16 视觉诊断/T17 文案校验)协调提交:
- 迁移 0006:rp_report(result JSONB 按阶段键承载中间态)/rp_input
 (derived_features 视觉中间态)/rp_share(V1 只建结构)
- ORM 三模型;错误码 40420/40920(文案无价格字样)
- 配置:report_pipeline_stages(dotted path 动态装配)+ vision/tier
- packages/report/pipeline.py:Stage Protocol + ReportContext(三任务共同契约,
  动态 import 的动机与代价写在 docstring)
- 路由桩 reports.py 已注册
This commit is contained in:
xsl
2026-08-14 21:51:05 +08:00
parent 7fb1e4514f
commit d974526904
8 changed files with 263 additions and 1 deletions
+75
View File
@@ -0,0 +1,75 @@
"""Report tables for the Sprint-3 (M3) parallel batch (架构文档 §6.2 report 域).
三张表一次落齐,T15(编排)不再写迁移:
rp_report — result JSONB 承载流水线中间态与最终产物(按 stage 键增量更新)
rp_input — questionnaire / photo_meta / derived_features(视觉中间态,T-P3-09)
rp_share — V1 只建结构(路由属后续任务)
Reversible: 是。
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision: str = "0006"
down_revision: str | None = "0005"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_table(
"rp_report",
sa.Column("report_id", sa.BigInteger(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=False),
sa.Column("tier", sa.String(length=8), nullable=False, server_default=sa.text("'basic'")),
sa.Column("status", sa.String(length=16), nullable=False, server_default=sa.text("'pending'")),
sa.Column("version", sa.Integer(), nullable=False, server_default=sa.text("1")),
sa.Column("result", postgresql.JSONB(), nullable=False),
sa.Column("model_info", postgresql.JSONB(), nullable=False),
sa.Column("content_code", sa.String(length=32), nullable=True),
sa.Column("rendered_asset_key", sa.String(length=255), nullable=True),
sa.Column("cost_cents", sa.Numeric(14, 6), nullable=False, server_default=sa.text("0")),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint("report_id"),
)
op.create_index("ix_rp_report_user_created", "rp_report", ["user_id", "created_at"])
op.create_table(
"rp_input",
sa.Column("input_id", sa.BigInteger(), nullable=False),
sa.Column("report_id", sa.BigInteger(), nullable=False),
sa.Column("questionnaire", postgresql.JSONB(), nullable=False),
sa.Column("photo_meta", postgresql.JSONB(), nullable=False),
sa.Column("derived_features", postgresql.JSONB(), nullable=False),
sa.PrimaryKeyConstraint("input_id"),
sa.UniqueConstraint("report_id", name="uq_rp_input_report"),
sa.ForeignKeyConstraint(["report_id"], ["rp_report.report_id"], ondelete="CASCADE"),
)
op.create_table(
"rp_share",
sa.Column("share_id", sa.BigInteger(), nullable=False),
sa.Column("report_id", sa.BigInteger(), nullable=False),
sa.Column("share_token", sa.String(length=64), nullable=False),
sa.Column("expire_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("view_count", sa.Integer(), nullable=False, server_default=sa.text("0")),
sa.Column("revoked", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.PrimaryKeyConstraint("share_id"),
sa.UniqueConstraint("share_token", name="uq_rp_share_token"),
sa.ForeignKeyConstraint(["report_id"], ["rp_report.report_id"], ondelete="CASCADE"),
)
def downgrade() -> None:
op.drop_table("rp_share")
op.drop_table("rp_input")
op.drop_index("ix_rp_report_user_created", table_name="rp_report")
op.drop_table("rp_report")