Files
pear-admin-flask/migrations/versions/a47a5d2a3f1b_add_site_nav_click_table.py
T
bwstudio be99b8a925 feat(data): 数据库搬到 data/ 目录 + SQLite WAL + 单卷绑定挂载
- config.py 新增 PEAR_DATA_DIR(默认 <项目根>/data),SQLite 库、Flask-Session
  统一收进 DATA_DIR;不设时本地开发行为不变
- applications/__init__.py 顶部先 load_dotenv,保证 .env 里的 PEAR_DATA_DIR
  在 config 导入时就生效
- extensions/init_sqlalchemy.py 接 SQLite connect 事件:WAL + synchronous=NORMAL
  + busy_timeout=30s(多 worker 写不再撞锁)
- config.py 注入 SQLALCHEMY_ENGINE_OPTIONS(pool_pre_ping + SQLite connect_args
  超时;MySQL 路径下自动跳过 connect_args 防止参数错误)
- 新增 alembic 迁移 a47a5d2a3f1b 建 site_nav_click(含 anon_id 字段),
  解决之前该表只由 db.create_all 建、不在迁移链里的隐患
- .gitignore / .dockerignore 加 data/ 排除规则(且把 migrations/ 从
  ignore 里重新放行 —— 否则新加的迁移进不了库)
- NAS Docker 部署全面重写:
  - compose 唯一绑定挂载 ./data:/app/data,宿主 File Station 看得见
  - start.sh 自检 data 目录 + 子目录;workers 默认改为 1 threads 8
    (SQLite 写串行,单进程最稳)
  - 新增 backup.sh:sqlite3 .backup 在线热备(确保 WAL 一致性)+ 压缩
    + 保留 14 天
  - Dockerfile 安装 sqlite3 客户端备用,建 /app/data 子目录并 chown
- docs/deployment-nas.md 重写数据持久化章节(路径表 + 热备命令 +
  恢复步骤 + 整库迁移 tar/untar 流程)
2026-09-06 19:43:28 +08:00

45 lines
2.1 KiB
Python

"""手动迁移:建 site_nav_click(之前是 db.create_all() 建的,不在 alembic 里)。
现有 DB 已经有一张同名的表,迁移以空操作为主;对空 DB(首次部署 / 新容器)会建表。
"""
from alembic import op
import sqlalchemy as sa
revision = 'a47a5d2a3f1b'
down_revision = 'afa5cfb23a85'
branch_labels = None
depends_on = None
def upgrade():
bind = op.get_bind()
inspector = sa.inspect(bind)
if 'site_nav_click' in inspector.get_table_names():
# 已存在的 DB:表在,但 alembic 视角下没版本记录 → 跳过 DDL
# 同时保证 anon_id 列存在(方案 C 升级路径)
cols = {c['name'] for c in inspector.get_columns('site_nav_click')}
with op.batch_alter_table('site_nav_click') as batch:
if 'anon_id' not in cols:
batch.add_column(sa.Column('anon_id', sa.String(length=36), nullable=True, comment='匿名访客ID'))
batch.create_index('ix_site_nav_click_anon_id', ['anon_id'], unique=False)
return
op.create_table(
'site_nav_click',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='记录ID'),
sa.Column('nav_id', sa.Integer(), nullable=False, comment='被点击的导航ID'),
sa.Column('anon_id', sa.String(length=36), nullable=True, comment='匿名访客ID'),
sa.Column('day', sa.String(length=10), nullable=True, comment='日期 YYYY-MM-DD'),
sa.Column('ip', sa.String(length=64), nullable=True, comment='访客IP'),
sa.Column('clicked_at', sa.DateTime(), nullable=True, comment='点击时间'),
sa.PrimaryKeyConstraint('id'),
)
with op.batch_alter_table('site_nav_click') as batch:
batch.create_index('ix_site_nav_click_nav_id', ['nav_id'], unique=False)
batch.create_index('ix_site_nav_click_anon_id', ['anon_id'], unique=False)
batch.create_index('ix_site_nav_click_day', ['day'], unique=False)
batch.create_index('ix_site_nav_click_clicked_at', ['clicked_at'], unique=False)
def downgrade():
op.drop_table('site_nav_click')