Files
yunke/yunke-server/migrations/versions/a1b2c3d4e5f6_voice_clip_table.py
T
浅念 d9060b6920 feat(api): 长语音 voice_clip 后端(model+migration+scraper+sync+endpoint)
- VoiceClip model (BigInteger PK voice_clip_id, structured cols + raw_json)
- Alembic migration a1b2c3d4e5f6 (chains from 8c4f1d2a9e6b)
- scrape_voice_clips(client, user_id, *, channel, page_size) per-employee scraper
  over POST /wechatweb/call/list {pageNum, pageSize, userId}; channel stamped
  locally (wechat/whatsapp), startTime/endTime ms-epoch string -> int
- upsert_voice_clip repo + start_time/end_time/mp3_duration int coercion
- sync_voice_clips orchestrator: iterates distinct user_id across
  WechatAccount + WhatsappAccount, stamps channel per account table
- scheduler ENTITY_FUNCS['voice_clips'] + added to daily 'all' job
- GET /api/voice-clips (user_id exact beats user_real_name fuzzy; channel;
  company_code; start/end window on end_time; ordered end_time desc)
- VoiceClipOut schema + router registered in main.py
- tests: scraper (5) + API (11) = 16 new; full suite 370 green

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-10 18:12:28 +08:00

50 lines
1.9 KiB
Python

"""voice_clip table
Revision ID: a1b2c3d4e5f6
Revises: 8c4f1d2a9e6b
Create Date: 2026-08-10 00:00:00.000000
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'a1b2c3d4e5f6'
down_revision: str | Sequence[str] | None = '8c4f1d2a9e6b'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
op.create_table('voice_clip',
sa.Column('voice_clip_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('user_id', sa.String(length=64), nullable=False),
sa.Column('user_real_name', sa.String(length=255), nullable=True),
sa.Column('wx_id', sa.String(length=128), nullable=True),
sa.Column('talker', sa.String(length=128), nullable=True),
sa.Column('status', sa.String(length=32), nullable=True),
sa.Column('is_send', sa.Boolean(), nullable=True),
sa.Column('company_code', sa.String(length=64), nullable=True),
sa.Column('start_time', sa.BigInteger(), nullable=True),
sa.Column('end_time', sa.BigInteger(), nullable=True),
sa.Column('oss_file_name', sa.String(length=1024), nullable=True),
sa.Column('mp3_duration', sa.Integer(), nullable=True),
sa.Column('duration_format', sa.String(length=32), nullable=True),
sa.Column('channel', sa.String(length=16), nullable=False),
sa.Column('raw_json', sa.JSON(), nullable=True),
sa.Column('fetched_at', sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint('voice_clip_id')
)
op.create_index('ix_voice_clip_user_time', 'voice_clip', ['user_id', 'start_time'], unique=False)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_index('ix_voice_clip_user_time', table_name='voice_clip')
op.drop_table('voice_clip')