"""飞书写入面权限预检 cron —— 让「这条路已经死了」在第一天就喊出来。 #3084(2026-08-19)把建号从 `POST /contact/v3/users`(scope `contact:user:write`, 早就有)换成 `POST /directory/v1/employees`(scope `directory:employee:write`, **从没开通过**)。没有任何东西会在真正需要写之前去验一次权限,而接下来 12 天恰好 没有新人需要建号 —— 于是建号这条路死了 12 天,每天的同步却全绿(更新走的是另一个 scope,一直好使)。它最终是被一个真人踩出来的:P10025 入职当天没有飞书账号。 这个 cron 每天问一次「我还写得动吗」,和有没有人入职无关。探针是对一个**故意非法的 employee_id** 发 PATCH:改不动任何东西,而且判据明确 —— 这个端点**先查权限再校验 id**(已对生产实测): 99991672 → 权限没了 99992360 → invalid user_id,即已经过了权限闸 缺权限时 handler **raise**,由定时任务框架的 `maybe_notify_failure` 去 page IT —— 复用既有出口,不新建告警通道。 Revision ID: 580 Revises: 579 """ from __future__ import annotations import uuid import sqlalchemy as sa from alembic import op revision = "580" down_revision = "579" branch_labels = None depends_on = None JOB_NAME = "identity_sync.feishu_write_scope_preflight" def upgrade() -> None: bind = op.get_bind() if not sa.inspect(bind).has_table("scheduled_jobs"): return exists = bind.execute( sa.text("SELECT 1 FROM scheduled_jobs WHERE name = :n LIMIT 1"), {"n": JOB_NAME}, ).scalar() if exists: return bind.execute( sa.text( """ INSERT INTO scheduled_jobs (id, name, display_name, description, handler_key, trigger_type, interval_seconds, cron_expr, timezone, enabled, config, run_count, failure_count, created_at, updated_at) VALUES (:id, :name, :display_name, :description, :handler_key, 'cron', NULL, :cron, 'Asia/Shanghai', 1, '{}', 0, 0, NOW(), NOW()) """ ), { "id": str(uuid.uuid4()), "name": JOB_NAME, "display_name": "飞书 · 建号权限预检", # 每天 07:30(本地时区)。一次探针 = 一次 HTTP 调用,频率的依据是 # 「权限被撤掉之后我们最多蒙在鼓里多久」,不是负载。放在早上是为了 # 让 IT 在当天的入职窗口(各地本地零点)之前就拿到告警。 "cron": "30 7 * * *", "description": ( "每天探一次飞书应用是否仍持有 Directory 建号写权限" "(对非法 employee_id 发 PATCH,只验权限不改数据)。" "缺权限时任务失败并通过定时任务失败通知 page IT —— " "避免建号面静默失效到下一个新人入职才被发现(#3084)。" ), "handler_key": JOB_NAME, }, ) def downgrade() -> None: bind = op.get_bind() if not sa.inspect(bind).has_table("scheduled_jobs"): return bind.execute( sa.text("DELETE FROM scheduled_jobs WHERE name = :n"), {"n": JOB_NAME}, )