- 迁移 15:upload(三态)、file_asset(sha256/存储键/pending-ready-quarantined)、media_job(四类五态+进度+取消)、voice_consent(用途/期限/撤回) - 上传三步流(契约第 9 节开发档):POST uploads(MIME 白名单+大小约束)→ PUT data(真实写 storage/ 落盘、sha256 校验)→ completions 转 ready——未完成文件不可被业务引用 - 文件内容取回(开发档本地;正式部署换短期签名 URL,存储键不变) - 媒体任务:ASR 输入必须 ready 音频;后台执行经注入 Provider(测试 mock 确定性转写);无 Provider 显式失败(fail-closed);取消幂等 - 声契:TTS/克隆需有效声契(未撤回未过期);撤回后新任务 403(ADR 媒体域“撤回停止新生成”) - CI(WP-14 起步):三任务流水线——backend(pgvector 服务容器 + ruff/mypy/pytest)、engine(CMake 警告即错误 + ctest)、openapi(Redocly lint) - 测试 91 项全绿(+5)
100 lines
5.3 KiB
Python
100 lines
5.3 KiB
Python
"""媒体:上传、文件、任务与声契
|
|
|
|
Revision ID: 52be875c14a4
|
|
Revises: f7bf2b464f1f
|
|
Create Date: 2026-09-12 10:47:53.573208
|
|
|
|
"""
|
|
from collections.abc import Sequence
|
|
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
from alembic import op
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = '52be875c14a4'
|
|
down_revision: str | Sequence[str] | None = 'f7bf2b464f1f'
|
|
branch_labels: str | Sequence[str] | None = None
|
|
depends_on: str | Sequence[str] | None = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
"""Upgrade schema."""
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.create_table('media_job',
|
|
sa.Column('id', sa.String(), nullable=False),
|
|
sa.Column('domain_id', sa.String(), nullable=False),
|
|
sa.Column('kind', sa.String(), nullable=False),
|
|
sa.Column('input', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
sa.Column('output', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
|
sa.Column('status', sa.String(), nullable=False),
|
|
sa.Column('progress', sa.Integer(), nullable=False),
|
|
sa.Column('error', sa.String(), nullable=True),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.CheckConstraint("kind IN ('asr', 'tts', 'image_gen', 'image_edit')", name=op.f('ck_media_job_kind_values')),
|
|
sa.CheckConstraint("status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')", name=op.f('ck_media_job_status_values')),
|
|
sa.ForeignKeyConstraint(['domain_id'], ['personal_domain.id'], name=op.f('fk_media_job_personal_domain'), ondelete='cascade'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_media_job'))
|
|
)
|
|
op.create_index(op.f('ix_media_job_domain_id'), 'media_job', ['domain_id'], unique=False)
|
|
op.create_table('upload',
|
|
sa.Column('id', sa.String(), nullable=False),
|
|
sa.Column('domain_id', sa.String(), nullable=False),
|
|
sa.Column('filename', sa.String(), nullable=False),
|
|
sa.Column('media_type', sa.String(), nullable=False),
|
|
sa.Column('size_limit', sa.Integer(), nullable=False),
|
|
sa.Column('status', sa.String(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.CheckConstraint("status IN ('pending', 'completed', 'failed')", name=op.f('ck_upload_status_values')),
|
|
sa.ForeignKeyConstraint(['domain_id'], ['personal_domain.id'], name=op.f('fk_upload_personal_domain'), ondelete='cascade'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_upload'))
|
|
)
|
|
op.create_index(op.f('ix_upload_domain_id'), 'upload', ['domain_id'], unique=False)
|
|
op.create_table('file_asset',
|
|
sa.Column('id', sa.String(), nullable=False),
|
|
sa.Column('domain_id', sa.String(), nullable=False),
|
|
sa.Column('upload_id', sa.String(), nullable=True),
|
|
sa.Column('filename', sa.String(), nullable=False),
|
|
sa.Column('media_type', sa.String(), nullable=False),
|
|
sa.Column('size_bytes', sa.Integer(), nullable=False),
|
|
sa.Column('sha256', sa.String(), nullable=True),
|
|
sa.Column('storage_key', sa.String(), nullable=False),
|
|
sa.Column('status', sa.String(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.CheckConstraint("status IN ('pending', 'ready', 'failed', 'quarantined')", name=op.f('ck_file_asset_status_values')),
|
|
sa.ForeignKeyConstraint(['domain_id'], ['personal_domain.id'], name=op.f('fk_file_asset_personal_domain'), ondelete='cascade'),
|
|
sa.ForeignKeyConstraint(['upload_id'], ['upload.id'], name=op.f('fk_file_asset_upload'), ondelete='set null'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_file_asset'))
|
|
)
|
|
op.create_index(op.f('ix_file_asset_domain_id'), 'file_asset', ['domain_id'], unique=False)
|
|
op.create_table('voice_consent',
|
|
sa.Column('id', sa.String(), nullable=False),
|
|
sa.Column('domain_id', sa.String(), nullable=False),
|
|
sa.Column('character_id', sa.String(), nullable=True),
|
|
sa.Column('purpose', sa.String(), nullable=False),
|
|
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.ForeignKeyConstraint(['character_id'], ['character.id'], name=op.f('fk_voice_consent_character'), ondelete='cascade'),
|
|
sa.ForeignKeyConstraint(['domain_id'], ['personal_domain.id'], name=op.f('fk_voice_consent_personal_domain'), ondelete='cascade'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_voice_consent'))
|
|
)
|
|
op.create_index(op.f('ix_voice_consent_domain_id'), 'voice_consent', ['domain_id'], unique=False)
|
|
# ### end Alembic commands ###
|
|
|
|
|
|
def downgrade() -> None:
|
|
"""Downgrade schema."""
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.drop_index(op.f('ix_voice_consent_domain_id'), table_name='voice_consent')
|
|
op.drop_table('voice_consent')
|
|
op.drop_index(op.f('ix_file_asset_domain_id'), table_name='file_asset')
|
|
op.drop_table('file_asset')
|
|
op.drop_index(op.f('ix_upload_domain_id'), table_name='upload')
|
|
op.drop_table('upload')
|
|
op.drop_index(op.f('ix_media_job_domain_id'), table_name='media_job')
|
|
op.drop_table('media_job')
|
|
# ### end Alembic commands ###
|