"""通知提醒:幂等键 + 到期扫描的只读口子 Revision ID: 0017 Revises: 0016 Create Date: 2026-11-10 两件事: 1. **提醒的幂等键**。到期/逾期提醒由后台循环每几分钟扫一次,扫描本身有 advisory lock 保证只有一个进程在跑,但进程重启、时钟抖动、人工重跑都可能 让同一条提醒再写一次。`dedupe_key` + 部分唯一索引把"同一件事只提醒一次" 交给数据库保证,调用方用 `ON CONFLICT DO NOTHING` 即可。普通通知的 `dedupe_key` 为空,不参与唯一约束。 2. **到期扫描要跨租户列出待办**。提醒是平台级定时任务,扫的时候没有"当前工作区", `item` 的隔离策略会让它一行都看不到。这里照 0016 的做法加一条只在 `app.reminder_list = 'on'` 时生效的 **SELECT-only** 策略:扫描器能列出 全平台的截止时间,但写通知仍然必须逐个工作区绑定租户,改不了别人的数据。 """ from __future__ import annotations from collections.abc import Sequence import sqlalchemy as sa from alembic import op from app.roles import APP_ROLE revision: str = "0017" down_revision: str = "0016" branch_labels: Sequence[str] | None = None depends_on: Sequence[str] | None = None CREATE_REMINDER_POLICY = """ CREATE POLICY item_reminder_list ON item FOR SELECT USING (current_setting('app.reminder_list', true) = 'on') """ def upgrade() -> None: op.add_column("notification", sa.Column("dedupe_key", sa.String(120), nullable=True)) op.create_index( "uq_notification_dedupe", "notification", ["workspace_id", "user_id", "dedupe_key"], unique=True, postgresql_where=sa.text("dedupe_key IS NOT NULL"), ) op.execute("DROP POLICY IF EXISTS item_reminder_list ON item") op.execute(CREATE_REMINDER_POLICY) op.execute(f"GRANT SELECT ON item TO {APP_ROLE}") def downgrade() -> None: op.execute("DROP POLICY IF EXISTS item_reminder_list ON item") op.drop_index("uq_notification_dedupe", table_name="notification") op.drop_column("notification", "dedupe_key")