"""sys_user 加 session_jti 列(一次一码防重放,安全需求)。 Revision ID: 0038_session_jti Revises: 0037_drop_audit_target_check Create Date: 2026-09-17 变更要点: 1. sys_user 加 session_jti:当前有效会话的授权码代(JWT jti)——写操作成功后 轮换出新码并在响应头返回,旧码写操作即拒(SESSION_ROTATED); 2. 读操作不比对(REST 读重放无危害,轮询与写并发时在途 GET 不掉线); 3. 兼容:NULL = 未纳管(存量会话 / 轮换开关关闭期间登录),校验跳过; 4. 双引擎幂等:先探测列存在再加(MySQL 与达梦升级互不冲突)。 """ from __future__ import annotations from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "0038_session_jti" down_revision: str | None = "0037_drop_audit_target_check" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def _column_exists(table: str, column: str) -> bool: # 达梦 inspector 返回大写名,统一小写比较 inspector = sa.inspect(op.get_bind()) return column.lower() in { name.lower() for name in inspector.get_columns(table) } def upgrade() -> None: if _column_exists("sys_user", "session_jti"): return op.add_column("sys_user", sa.Column("session_jti", sa.String(length=64), nullable=True)) def downgrade() -> None: if not _column_exists("sys_user", "session_jti"): return op.drop_column("sys_user", "session_jti")