- Single Code-node sweep: desired = CP-active paths UNION active AccessGrants; expiry excluded -> role/update rewrite; optional auto-disable (env-gated, default off) - disable guard: only when had scopes before (never fresh empty-role customer) - loop guard: no NocoDB Customers.status flip (avoids W1 re-provision loop) - test_w3.js mock-verified both AUTO_DISABLE modes; gen_w3.js builds JSON from w3-compute.js
63 lines
4.0 KiB
Markdown
63 lines
4.0 KiB
Markdown
# 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 验收。
|