feat(notify): 站内通知补齐完成、到期逾期与知识事件
CI / backend (push) Waiting to run

This commit is contained in:
Steven
2026-11-10 13:47:56 +08:00
parent 4b7627d70e
commit f54df70811
15 changed files with 791 additions and 32 deletions
@@ -0,0 +1,60 @@
"""通知提醒:幂等键 + 到期扫描的只读口子
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")