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

63 lines
4.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 验收。