"""安全宣贯:催办 cron(无 DDL)。 只播种一个 `scheduled_jobs` 行。催办的**节奏**不落在这里 —— `app.notifications.cadence` 已经是平台的催办阶梯原语,本域在 `reminders.install_ladder()` 里给自己的两个 template_key 注册**更长**的 阶梯(7 / 7 / 14 天,之后每 14 天;主管摘要 14 天,之后每 28 天),而不是 自己数天数。 平台默认阶梯是 1/1/1/2/3/5 然后每周 —— 那个默认对「你的笔记本下落不明」 是对的,对「请读一份漫画」是错的:对必修阅读天天催,正是让那条**关不掉的** 安全类别被整个静音的方式,而之后连真正不能静音的流程也没人读了。**阶梯只能 拉长,不能收紧**。 第一次催办在发出后 **7 天**(材料几分钟就能读完,大多数人第一周就做了; 更早催是把渠道花在本来就会照做的人身上),三周后给主管发**一次摘要**—— 一张卡列全部下属,不是一人一张。 Revision ID: 599 Revises: 598 ⚠️ 迁移号要对着 `origin/main` 抢,而且每次 push 之前重新抢一次。 """ from __future__ import annotations import uuid import sqlalchemy as sa from alembic import op revision = "599" down_revision = "598" branch_labels = None depends_on = None JOB_KEY = "security_awareness.campaign_reminders" _JOB_DESC = ( "每天看一次哪些人还欠着必修宣贯的确认,并按递减的阶梯催办:发出后 7 天" "才第一次催,之后 7 / 7 / 14 天,再之后每 14 天一次,只要这件事还开着" "就不停。三周后给主管发一次摘要(一张卡列全部下属,不是一人一张)。\n" "每 tick 有条数上限、最久未处理优先,被压下的条数写进摘要。员工一确认" "就立刻销账,否则他下一次因为同类事情被催时会继承这次的计数。\n" "已结束(closed)的活动不催 —— 结束这个动作本身就是停止追问。" ) def _has_table(bind, name: str) -> bool: return bind.execute( sa.text( "SELECT 1 FROM information_schema.tables " "WHERE table_schema = DATABASE() AND table_name = :n" ), {"n": name}, ).scalar() is not None def upgrade() -> None: bind = op.get_bind() if not _has_table(bind, "scheduled_jobs"): return bind.execute( sa.text( "INSERT INTO scheduled_jobs " "(id, name, display_name, description, handler_key, " " trigger_type, cron_expr, timezone, enabled, config, " " run_count, failure_count, created_at, updated_at) " "SELECT :id, :name, :display, :desc, :handler, 'cron', " ":cron, 'Asia/Shanghai', 1, :config, 0, 0, NOW(), NOW() " "FROM DUAL WHERE NOT EXISTS " "(SELECT 1 FROM scheduled_jobs WHERE handler_key = :handler)", ), { "id": str(uuid.uuid4()), "name": JOB_KEY, "display": "安全宣贯 · 催办必修宣贯", "desc": _JOB_DESC, "handler": JOB_KEY, # 10:00 上海。上午而不是清早:一条在通勤路上到达的催办,被读到 # 的时候人已经在做别的事了。 "cron": "0 10 * * *", "config": "{}", }, ) def downgrade() -> None: bind = op.get_bind() if _has_table(bind, "scheduled_jobs"): bind.execute( sa.text("DELETE FROM scheduled_jobs WHERE handler_key = :k"), {"k": JOB_KEY}, )