# 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 验收。