Files
alist-serve-resource/n8n/W3-DESIGN.md
T
hoelee 2de82ff3af fix(W2/W3): expiry boundary — use <= today (expiry on today counts as expired)
Root cause: expires_at == today was NOT filtered (used '< today'), so a
product expiring ON today kept its resources in desired scopes -> no diff
-> no removal. Changed to '<='. Applied to W3 (CP + grant loops) and W2
(active filter). Also W2 previously had NO expiry filter on active CPs at
all.
2026-08-23 03:48:44 +08:00

4.0 KiB
Raw Blame History

W3 — Expiry Sync / 全量对账(schedule: every 5 min)

定时任务,不是 webhook。负责「到期移除 scope、恢复客户、至空则禁用」。 与 W1/W2 正交:W1/W2 是事件驱动(NocoDB 变更 → 重算单个 customer); W3 是周期性全量对账(扫所有 customer → desired vs actual → 修 AList / 回写 NocoDB)。 幂等规则 §28:get current → compare → apply diff,重跑不产生重复写。

触发

  • Schedule Trigger(n8n-nodes-base.scheduleTrigger),cron */5 * * * *。
  • 每次起手先 login AList 换新 token(JWT_EXPIRE=2h,不缓存)。

核心逻辑(desired-state)

对每个已有 alist_role_id 的 customer:

  1. 读该 customer 全部 CustomerProducts(where=(customer.Id,eq,${cid})&limit=1000)。
  2. desired paths = 仅取 status=active 且未过期 的 CP 的 Product→Resources alist_path 集合并。
    • 过期判定:status==='expired' 或 expires_at <= today,一律排除(含当天到期)。
    • 有效 CP 无 expires_at → 视为长期有效,保留。
  3. 读 AList role 当前 scopes(role/get?id=)。
  4. desired paths vs current paths → 差异即要改。
    • 到期移除:current 有、desired 无 → 从 scopes 移除(整体重写 role/update)。
    • 新生效加入:desired 有、current 无 → 加入。
  5. 有变 → POST /api/admin/role/update 整体重写 permission_scopes(幂等)。
  6. disable 政策(开关 ALIST_AUTO_DISABLE_ON_EXPIRE,默认 false):
    • 若 desired 为空 且 该 customer NocoDB status==='active' → 回写 status='expired',且(开关开时)user/update disabled=true。
    • 默认关 = 首轮只观察,不自动禁用任何客户,避免误伤。

实现形态(与 W2 的差异,需知晓)

  • W2(事件驱动):Code 节点算 → 独立 HTTP 节点 apply,单客户,逐步可见。
  • W3(后台对账):单个 Code 节点内完成 read + write(this.helpers.httpRequest), 每客户 try/catch 收集错误进 summary,最终返回一条 summary + 可选通知。 理由:① 无多 item 跨节点表达式陷阱(reconcile 输出 N 条会让 $('X').item.json 报错); ② 定时无人值守,原子化比逐步可见更稳;③ 幂等,重复跑无副作用。 代价:写操作在代码里,调试靠 per-customer errors[]。已权衡,接受。

Webhook loop 防护(关键)

  • NocoDB webhook 触发的是 W1(Customers) 和 W2(CustomerProducts)。
  • W3 回写要避开:
    • 改 CustomerProducts.sync_status → 会触发 W2 webhook;W2 自带 "skip if already synced" 短路,安全。但 W3 应在确无 diff 时完全不写,减少空触发。
    • 改 Customers.status→expired → 会触发 W1 webhook → 可能重新 provision → loop。 规避:W3 只在自己做了「去 scope 至空」时才翻 status;并且只设一个方向 (active→expired),同一同步周期内不回头。首期开关默认关,连 status 翻转也延后。
    • 铁律(照 PROJECT_STATE):不盲写 last_sync 时间戳;语义没变就不写。

待确认决策点

  1. disable 政策是否首期启用?(建议否,先观察 1–2 轮)
  2. 是否顺带物化 AccessGrants(W3 写 product/manual 源之外)?—— 挂在 W2 验证后那项确认。
  3. reconciliation 结果日志:不动 schema(AccessLogs 是用户行为审计),先只靠 n8n execution + summary。要不要新开 ReconcileLogs 表,验证后再说。

里程碑 / 测试

  • M1:以 dryRun=true 跑一次(只读,不写 AList/NocoDB),输出「将改的 diff」清单。
  • M2:造一条 expires_at=昨天 的 CP,观察 W3 将其 scope 移除(开关仍关,禁用不生效)。
  • M3:开 ALIST_AUTO_DISABLE_ON_EXPIRE=true,造一键全到期 customer,观察 role scopes 清空 + user disabled + status=expired。
  • 全部用 n8n execution error + summary JSON 验收。