fix(W1): self-write-back suppression via old/new field comparison
- Replace brittle sync_status==='synced' loop guard with NocoDB v2 old/new row comparison (previous_rows vs rows). - businessChanged() = business field move OR sync_required set; echo (bookkeeping-only delta) -> skip_writeback_echo, zero AList mutation. - writeBack() value-change guard: GET live record, PATCH only fields whose value actually changes (no-change -> no PATCH -> no webhook). - sync_required Checkbox honored as force-reprocess flag, cleared on write-back. - Extract-id node passes previousRow/newRow through Load Env to lifecycle. - W1 lifecycle test now covers echo suppression (26 assertions). - Customers.status still NOT consumed by permission calc (TBD semantics). - W2/W3 effective-grants semantics unchanged.
This commit is contained in:
+22
-9
@@ -42,13 +42,17 @@ Base: **`AlistAccess`**(`p3lzkresswyowqg`),与旧 `accessDB` 完全隔离
|
||||
|
||||
### 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)
|
||||
- 流程:Webhook → Extract customerId(同时透传 webhook 的 old/new rows)→ 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
|
||||
- **幂等**:见 `n8n/test_w1_lifecycle.js`(26 断言全过,含 echo/suspend 复用/suspend→active resume 重算不恢复陈旧/sync_required 强制重算)
|
||||
- **Loop guard(2026-08-23 重写,核心)**:不再用 `sync_status==='synced'`,改用 **old/new field comparison + `sync_required` 标志**:
|
||||
- `businessChanged()` = 业务字段(status/username/password/display_name/email/notes)在 `previous_rows`→`rows` 间变化,**或** `sync_required` 置位
|
||||
- echo(只动了 alist_user_id/alist_role_id/sync_*/last_sync*)→ 立即 `skip_writeback_echo`,零 AList 变更、零 PATCH
|
||||
- **write-back 值比对守卫**:writeBack() 先 GET 当前记录,只 PATCH 值真会变的字段;值没变 → 无 PATCH → 无 webhook → 从物理上掐断 loop
|
||||
- 写回时始终清 `sync_required: false`
|
||||
- base_path 硬编码 `'/'`;空 role 数组先建 role 再建 user
|
||||
|
||||
### 3. W2 CustomerProduct 权限同步 — **实测跑通** ✅
|
||||
@@ -69,14 +73,16 @@ Base: **`AlistAccess`**(`p3lzkresswyowqg`),与旧 `accessDB` 完全隔离
|
||||
- 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 断言)——全过
|
||||
- **测试**:`test_effective_grants.js`(共享算法 15 断言)、`test_w3_refactored.js`(W3 重构回归 13 断言)、`test_w1_lifecycle.js`(W1 lifecycle 幂等 26 断言)——全过
|
||||
|
||||
## 🔴 下一步(按序)
|
||||
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 已推)
|
||||
1. **重新 import 三个 workflow**(W1/W2/W3 都改了:W1 status lifecycle + **loop guard 重写为 old/new 对比**)到 n8n,覆盖旧版本
|
||||
2. **实测 loop 修复**:NocoDB 改 customer.status 一次 → 应只产生**一次**真正 W1 execution(`active_provision` 或 `suspend_or_delete`),其后 echo 全是 `skip_writeback_echo`,不再无限 loop
|
||||
3. **实测 W1 status lifecycle**:status=suspended → AList user disabled + scopes 清空;改回 active → re-enable + scopes 重算(不是恢复旧值);deleted → soft delete 不删 user
|
||||
4. **⚠️ 待决策**:`Customers.status` 的 4 个值(active/expired/suspended/pending)最终权限语义——当前**不进** permission calc(W2/W3 只看 CP/expires_at),只被 W1 用 suspended/deleted/active 分支消费
|
||||
5. **(Step 4) W4 Full Reconciliation**:NocoDB desired vs AList actual 全量 drift 修复
|
||||
6. (Step 5 之后) 暂不做 hard delete(W6)
|
||||
7. 本地 git 留档 + 验证稳定后推 Gitea(git.hoelee.com;凭据已脱敏 + 历史已重写)
|
||||
|
||||
## 🐛 已沉淀的坑(勿再踩)
|
||||
- **AList**:
|
||||
@@ -114,6 +120,13 @@ Base: **`AlistAccess`**(`p3lzkresswyowqg`),与旧 `accessDB` 完全隔离
|
||||
} };
|
||||
```
|
||||
|
||||
- **Webhook write-back loop(2026-08-23 已修)**:
|
||||
- 症状:n8n 写回 NocoDB(sync_status 等)→ 触发 webhook → 又写回 → 无限循环;且 `sync_status==='synced'` 做 guard 不可靠(suspend 分支也写 synced)
|
||||
- 正确做法:**old/new field comparison**。NocoDB v2 webhook payload `data.rows`(改后)+ `data.previous_rows`(改前)。只有业务字段(status/username/password/display_name/email/notes)变化才是 admin 变更;只动了 alist_user_id/alist_role_id/sync_*/last_sync* = echo → 短路
|
||||
- 加 `sync_required`(Checkbox)作为强制重算开关:admin 需要强制 provision 时勾选;写回时始终清 false
|
||||
- **writeBack 值比对守卫**:PATCH 前先 GET 当前值,只写真会变的字段;值没变 → 不发 PATCH → 不触发 webhook(物理层面掐断 loop)
|
||||
- extract-id 节点必须透传 `previousRow`/`newRow`(经 Load Env 转发)到 lifecycle Code 节点
|
||||
|
||||
## 📁 项目结构
|
||||
```
|
||||
alist-serve-resource/
|
||||
|
||||
Reference in New Issue
Block a user