feat(W1): status lifecycle (suspend/delete/resume) + shared effective-grants lib

- Extract calculateEffectiveGrants into n8n/shared/effective-grants.js
  (single source of truth), inlined at build time into W1/W2/W3 via
  gen_w*.js + inline_shared.js — removes the duplicated algorithm.
- W1 now provisions with a status branch:
  suspended/deleted -> disable AList user + clear role scopes (soft delete)
  active -> create/update user+role, RECOMPUTE desired scopes (never restore
  stale), re-enable
- Loop guard corrected: check AList user LIVE disabled state via
  GET /api/admin/user/get (sync_status alone breaks suspend->resume).
- W2/W3 rebuilt on shared lib (identical behaviour).
- Tests: test_effective_grants.js (15), test_w3_refactored.js (13),
  test_w1_lifecycle.js (23) — all green.
- Drop superseded w2-compute.js / w3-compute.js / test_w3.js.
This commit is contained in:
2026-08-23 04:59:43 +08:00
parent 870a702f7b
commit 3ecee3099e
18 changed files with 1216 additions and 1109 deletions
+40 -29
View File
@@ -1,4 +1,4 @@
# Project State (Handoff) — 2026-08-22
# Project State (Handoff) — 2026-08-23
> 交接文档:压缩/新会话后,按此文档续做。细节见各 doc。
@@ -40,37 +40,43 @@ Base: **`AlistAccess`**(`p3lzkresswyowqg`),与旧 `accessDB` 完全隔离
- 表/字段/链接列 ID 全表:`docs/DATABASE_SCHEMA.md`(已更新为 7 表)
- NocoDB API token(workspace 级,来自 nc_api_tokens 表 n8n 那行):在 `docs/API_INTEGRATION.md` / SETUP.md 有记(或查 NocoDB UI)
### 2. W1 客户开通 workflow — **已实测跑通** ✅
- 文件:`n8n/W1-customer-provision.json`
- 流程:Webhook → Extract customerId → **Load Env(step 2, 用 $env)** → Fetch 记录 → 判断 create/update → Create Role → Find Role Id → Create User(role:[roleId]) → Find User Id → Write Back
- 关键:AList user/create 传 **空 role 数组会被拒**(自动填 guest→报 "admin or guest user can not be created")。必须先 Create Role 拿自增 id,再 Create User 带 `role:[id]`
- **base_path 已硬编码 `'/'`**(不是 `/Class`,否则 JoinPath 双重前缀 → 403):commit 1ac1f17
- **Loop guard**:新增 "Skip if already synced" IF 节点(sync_status=synced 时短路,不写回 NocoDB → 打破 webhook 循环);同时去掉 last_sync 写入(时间戳每次必变会破坏 guard)
- Load Env 已同步为 `$env` 版本(process.env 在 Code 节点不可用):commit 58c2f57
### 2. W1 客户开通 + Status Lifecycle — **实测跑通** ✅
- 文件:`n8n/W1-customer-provision.json`(由 `n8n/w1-body.js` + 共享函数库经 `gen_w1.js` 生成)
- 流程:Webhook → Extract customerId → Load Env → **单 Code 节点 "Provision & Apply Lifecycle"**(含 login + status 分支 + write-back)
- **Step 3 新增 status lifecycle**:
- `suspended`/`deleted` → disable AList user + 清空 role scopes(soft delete,保留 user + NocoDB 历史)
- `active` → provision(建/更新 user+role)→ **重算 desired scopes(复用共享 calculateEffectiveGrants,不恢复旧值)** → re-enable
- 其他 → no-op
- **幂等**:见 `n8n/test_w1_lifecycle.js`(23 断言全过,含 echo/suspend 复用/suspend→active resume 重算不恢复陈旧)
- 关键坑:loop guard 不能只看 `sync_status==='synced'`(suspend 也写 synced),要**查 AList user 实际 disabled 状态**(`GET /api/admin/user/get?id=`)——disabled 说明是 resume,须走完整 provision
- base_path 硬编码 `'/'`;空 role 数组先建 role 再建 user
### 3. W2 CustomerProduct 权限同步 — **已改好,需重新 import 测试** ⏳
- 文件:`n8n/W2-customerproduct-sync.json`
- 流程:Webhook → Extract customerProductId → Load Env → **Compute Effective Grants**(登录 AList→读 customer→读全部 active CP→读 Product 的 M2M Resources→**per-Resource MAX(expires_at)** 合并→读 AList role 现状→diff→算 newScopes) → Skip if no change → AList Update Role Scopes → Write Back CP Sync Status
- **已修 bug(commit 36a6b3b)**:查询 active CP 的 where 语法从 `(customer,eq,${id})` → `(customer.Id,eq,${id})`。NocoDB link 字段用 display name 直接 eq 返回 0 条;必须用 `customer.Id` 子字段或外键列 `nc_jitu___Customers_id`
- **已修 bug**:link API 返回 `{Id,name}` 不含 `alist_path` → 新增 resById 反查(先加载全量 Resources 按 Id 索引,再按 link 结果的 Id 反查 alist_path)
- **Loop guard**:新增 "Skip if no change" IF + No-op 死端(scopes 无 diff 且已 synced 时短路);去掉 last_sync
- ⚠️ 待用户重新 import + 测试
### 3. W2 CustomerProduct 权限同步 — **实测跑通** ✅
- 文件:`n8n/W2-customerproduct-sync.json`(`n8n/w2-body.js` + 共享库 → `gen_w2.js`)
- 流程:Webhook → Extract customerProductId → Load Env → **Compute Effective Grants & Apply**(单 Code 节点调共享 `calculateEffectiveGrants`)→ Skip if no change → No-op
- customer link 为空时优雅跳过(`skipWriteBack:true`,不回写,补填后再触发)
- expiry 边界 `<= today`(到期当天视为过期)
### 4. W3 Expiry Sync(全量对账)— **已实现,待 import + dry-run 测试** 🆕
- 文件:`n8n/W3-expiry-sync.json`(Schedule `*/5` → 单 Code 节点"Reconcile & Apply")
- 设计:`n8n/W3-DESIGN.md`;参考实现 `n8n/w3-compute.js`(自包含,read+write 都在 Code 内)
- **核心逻辑**:对每个有 `alist_role_id` 的 customer → desired = 有效 CP 派生路径 ∪ 有效 AccessGrants 路径(**并集,防止整写 scopes 清掉 manual 授权**)→ 过期排除 → diff → `role/update` 整体重写 → 选项 disable
- **Env 开关**:`ALIST_AUTO_DISABLE_ON_EXPIRE`(默认 false=只观察)、`W3_DRY_RUN`(true=只打印计划不写)
- **disable 护栏**:仅当 `nowEmpty && status=active && currentPaths>0`(原本有 scope 现在空)才禁用;空 role 的新客户不动(否则刚开通没买产品就被禁,W2 不会重新启用)
- **已本地验证**:`n8n/test_w3.js` mock 数据跑通 OFF/ON 两模式,断言全过(manual 授权保留、到期清除、disable 正确、空 role 客户不动)
- **loop 防护**:只写 AList user disabled + role scopes;**不**翻 NocoDB Customers.status(Customers update webhook→W1 可能 re-provision/re-enable=loop)
### 4. W3 Expiry Sync(全量对账)— **实测跑通** ✅
- 文件:`n8n/W3-expiry-sync.json`(`n8n/w3-body.js` + 共享库 → `gen_w3.js`)
- Schedule `*/5` → 单 Code 节点 "Reconcile & Apply"(逐客户调共享 `calculateEffectiveGrants` → diff → 应用)
- 修过 Bearer 前缀 bug(AList 要裸 token)+ expiry 边界 `<= today`
- Env:`W3_DRY_RUN`、`ALIST_AUTO_DISABLE_ON_EXPIRE`(默认 off)
### 5. 共享函数库(Step 3 核心重构)🆕
- **`n8n/shared/effective-grants.js`** = 单一事实源:`calculateEffectiveGrants(customerId)` + `ncGet`/`alistReq`/`linkId`
- 算法:desired = 有效 CP 派生路径 **∪** 有效 AccessGrants 路径;同 path 多 product 取 **MAX(expires_at)**;permission 取 readwrite 覆盖
- expiry 边界 `<= today`;status 过滤非 active
- **构建时内联**:`n8n/inline_shared.js` 的 `sharedSource()` 剥离 `module.exports`(`/* SHARED_EXPORT_BEGIN */` 之后),三个 `gen_w*.js` 把 body(`__SHARED_INLINE__` 占位)+ 共享源拼成 JSON
- **W1/W2/W3 零复制算法**:三处都调同一个 `calculateEffectiveGrants`
- **测试**:`test_effective_grants.js`(共享算法 15 断言)、`test_w3_refactored.js`(W3 重构回归 13 断言)、`test_w1_lifecycle.js`(W1 lifecycle 幂等 23 断言)——全过
## 🔴 下一步(按序)
1. **用户测试 W2**(import 覆盖 → 用例 2、3 → AList 前端实测客户登录看目录)→ 报错贴 execution 错误
1.5. **W3 测试**:import `W3-expiry-sync.json` → 设 `W3_DRY_RUN=true` 跑一次看计划 → 造过期 CP 观察 scope 移除 → 开关 auto-disable 做 M3(详见 W3-DESIGN 里程碑)
2. 验证通过后:**expiry scheduler**(每 5 分钟对账:全量 diff NocoDB↔AList,到期的移除 scope)—— 规格 §26/§37
3. 其余 workflow(W3-W6):Product/Resource 变更 → 重算受影响 customer;手动 sync;日志;通知
4. 本地 git 留档 → 验证稳定后推 Gitea(git.hoelee.com)
1. **重新 import 三个 workflow**(W1/W2/W3 都改了,共享库重构 + W1 status lifecycle)到 n8n,覆盖旧版本
2. **实测 W1 status lifecycle**:NocoDB 改 customer.status=suspended → 看 AList user disabled + scopes 清空;改回 active → 看 re-enable + scopes 重算(不是恢复旧值);deleted → soft delete 不删 user
3. **(Step 4) W4 Full Reconciliation**:NocoDB desired vs AList actual 全量 drift 修复(W3 已具备基础,W4 补 reconciliation 日志 + 每日全量)
4. (Step 5 之后) 暂不做 hard delete(W6)
5. 本地 git 留档 + 验证稳定后推 Gitea(git.hoelee.com;凭据已脱敏 + 历史已重写,tag v0.3-core-sync-working 已推)
## 🐛 已沉淀的坑(勿再踩)
- **AList**:
@@ -112,7 +118,12 @@ Base: **`AlistAccess`**(`p3lzkresswyowqg`),与旧 `accessDB` 完全隔离
```
alist-serve-resource/
├── docker-compose.yml / .env.example / .gitignore
├── n8n/ W1-customer-provision.json · W2-customerproduct-sync.json · SETUP.md · W2-DESIGN.md · w2-compute.js
├── n8n/
│ W1/W2/W3-*.json (generated) · w1/w2/w3-body.js (runtime logic)
│ shared/effective-grants.js (single source of truth)
│ gen_w1/w2/w3.js + inline_shared.js (build-time inlining)
│ test_effective_grants.js · test_w1_lifecycle.js · test_w3_refactored.js
│ SETUP.md · W2-DESIGN.md · W3-DESIGN.md
├── scripts/ create_nocodb_schema.py · healthcheck.sh
└── docs/ API_INTEGRATION.md · DATABASE_SCHEMA.md · INSTALL.md · CONFIGURATION.md · SECURITY.md · BACKUP_RESTORE.md · UPGRADE.md · TROUBLESHOOTING.md · PROJECT_STATE.md
```