61 lines
2.1 KiB
Python
61 lines
2.1 KiB
Python
"""通知提醒:幂等键 + 到期扫描的只读口子
|
|
|
|
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")
|