"""msg_daily_stats 日粒度会话汇总表(会话列表/5数字弹窗超时修复) Revision ID: a7d3e9f1c2b4 Revises: c53f90b1e6a4 Create Date: 2026-09-03 17:00:00.000000 内容 ---- sessions 与 staff-chat-stats 原先每次请求对 owner 全量 message 实时 GROUP BY——重账号 5.2 万条消息实测 3~5s(sessions)/6.9s(全时段 staff-chat-stats),在服务器 128MB buffer pool 下进一步被推过前端 axios 15s 超时(ECONNABORTED,2026-09-03 用户报「聊天记录页数据多时请求失败」)。 本表是预聚合层:每个「渠道×账号×会话×上海自然日」一行(收/发计数、当日 最后一条时间与方向)。维护:: * 写路径:``repos.upsert_messages`` 落库后自动重算本批触及的「会话×天」 (所有抓取路径——页面抓取器微信/WA/群、CLI 手动同步——都汇到该函数)。 * 读路径:两个端点在表有行时读它(毫秒级),空表回退旧实时聚合(部署 回填前的守卫)。 * 自愈:夜间 full_sync_all.sh 的 ``daily-stats`` 步骤全量重建,纠正绕过 钩子的写入(裸 UPDATE/回填脚本);部署回填也用同一条命令 (``python -m app.sync.cli daily-stats``)。 day 为 int yyyymmdd,口径一律 Asia/Shanghai(Python 计算,不用 SQL 日期 函数——from_unixtime 跟随 DB 会话时区,与今日数口径割裂)。 幂等:``sa.inspect`` 先查同名表,存在即跳过。SQLite/MySQL 通用。 """ from collections.abc import Sequence import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "a7d3e9f1c2b4" down_revision: str | Sequence[str] | None = "c53f90b1e6a4" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None _TABLE = "msg_daily_stats" def upgrade() -> None: bind = op.get_bind() insp = sa.inspect(bind) if insp.has_table(_TABLE): return op.create_table( _TABLE, sa.Column("channel", sa.String(length=16), primary_key=True), sa.Column("owner_account_id", sa.String(length=128), primary_key=True), sa.Column("peer_kind", sa.String(length=8), primary_key=True), sa.Column("peer_id", sa.String(length=128), primary_key=True), sa.Column("day", sa.Integer(), primary_key=True), sa.Column( "send_cnt", sa.Integer(), nullable=False, server_default=sa.text("0") ), sa.Column( "recv_cnt", sa.Integer(), nullable=False, server_default=sa.text("0") ), sa.Column("last_ts", sa.BigInteger(), nullable=True), sa.Column("last_is_send", sa.Boolean(), nullable=True), sa.Column( "updated_at", sa.DateTime(), nullable=False, server_default=sa.func.now() ), ) def downgrade() -> None: bind = op.get_bind() insp = sa.inspect(bind) if not insp.has_table(_TABLE): return op.drop_table(_TABLE)