# W2 — CustomerProduct 变更 → 同步 AList Role Scopes 事件驱动:NocoDB CustomerProducts 表 webhook → n8n webhook → 重取记录 → 全量重算该 customer 的 effective grants → diff AList role scopes → apply → 物化 AccessGrants + 回写 sync_status。 ## 核心逻辑(desired-state,规格 §19/§37) 1. webhook 只带 record Id,n8n `GET /api/v2/tables/{CustomerProducts}/records/{Id}` 重取完整记录。 2. 从记录解析 `customer` link(NocoDB link 字段返回关联行 Id + display value); 若返回对象数组,取第一个的 `Id`。 3. 拉取该 customer 所有 **status=active 且未过期** 的 CustomerProducts。 4. 每个 CP → `product` link → Product;再走 Products 的 M2M link 列 `c4ko4x8kwi16swg`(`GET /api/v2/tables/{Products}/links/c4ko4x8kwi16swg/records/{pid}`) 取该 Product 的全部 Resources(含 `alist_path`)。 5. 汇总:`alist_path -> MAX(expires_at)`(多 Product 重叠取最晚;规格 §18「取最大值」)。 8. 取 customer 的 `alist_role_id` → `GET /api/admin/role/get?id=` 读当前 scopes。 9. diff:desired paths vs 当前 scopes → 要加 / 要删。 10. `POST /api/admin/role/update` 整体重写 `permission_scopes`(幂等)。 11. 物化 AccessGrants:删掉该 customer 的 product 源 grants,重插 desired(manual 源保留不动)。 12. 回写 CustomerProducts 各记录 `sync_status=synced` + `last_sync`。 ## 关键 fact(已验证) - NocoDB link 字段在 v2 records GET 返回形如 `{"customer": {"Id": 5, "username": "..."}}` 或 `[{"Id":5}]`; Code 节点需兼容两种。 - AList role create/update 返回 `data:null`;role/get 可回读 scopes。 - `permission=0`(read-only bitmask);role scope 用 `{path, permission:0}`。 - M2M link col id: Products.Resources = `c4ko4x8kwi16swg`。 ## 首期不做 - expiry 定时扫描/移除(W1/W2 验证后再加) - 密码 change-detection 的 password_updated_at 触发逻辑(W1 已留字段)