- w4-body.js: ONE-DIRECTIONAL sync (NocoDB status = SOT, never reverse);
drift matrix: missing user/role (provision w/ username lookup to avoid
duplicates), dangling refs (adopt/recreate), disabled<->status, canonical
base_path '/' + permission 16384, scopes vs calculateEffectiveGrants,
username drift detect+log_only; verify-after-repair on every change;
password never pushed on repair (方案 C); dry-run via ?dryRun=true
- gen_w4.js: daily 03:00 cron + webhook alist-w4-reconcile; API-import-safe
(UUID node ids, no pinData, webhookId)
- ReconcileLogs table mktsb7le6fqr2u1 (add_reconcilelogs_table.py, idempotent,
v2 API); FK column nc_jitu___Customers_id for bt-link writes (v2 ignores
link column itself)
- test_w4_reconcile.js: 34 assertions / 12 mock worlds, ALL PASSED
- live-verified against production: dry-run (3 drifts + 1 error found),
real repair (3 repaired, verify all passed, 4 log rows), idempotent rerun
- NocoDB 2026.08.x pitfalls documented (v1 col endpoints gone, row delete
via body [{Id}]); n8n API import/activation pitfalls (activeVersionId)
- docs: W4-DESIGN.md new; DATABASE_SCHEMA 8 tables; N8N_WORKFLOWS, SETUP,
PROJECT_STATE updated
182 lines
7.9 KiB
Markdown
182 lines
7.9 KiB
Markdown
# NocoDB 数据结构(Phase 2b — 重建,8 表最终模型)
|
||
|
||
> Source of Truth = NocoDB。AList 只做 enforcement,不存业务授权数据。
|
||
> 本 schema 按《Hermes 调整指导》最终业务模型重建(2026-08-22)。
|
||
> 通过 NocoDB API 自动建表,无需手动点 UI。ReconcileLogs 于 2026-08-23 W4 新增。
|
||
|
||
- **Base**: `AlistAccess`(id `p3lzkresswyowqg`),独立新 base,与旧 `accessDB` 隔离。
|
||
- **MySQL source**: mysql-server 上的 `alistaccess` schema(由 NocoDB 自动管理)。
|
||
- **API token**: workspace token(`nc_api_tokens` 表内 `n8n` token),`xc-token` header。
|
||
|
||
## 表 model_id(n8n v2 API 硬编码用)
|
||
|
||
| 表 | model_id |
|
||
|---|---|
|
||
| Customers | `maqb9zgvza2te3k` |
|
||
| Products | `molyb3pxv81l75g` |
|
||
| Resources | `m6cm7tkxzd8m18c` |
|
||
| CustomerProducts | `mvyqbv8ao7u6bct` |
|
||
| AccessGrants | `mxdx3yhfcyej3ao` |
|
||
| AccessLogs | `ml12zcyzlehgi21` |
|
||
| Purchases | `mwp12z9lcohhsuk` |
|
||
| ReconcileLogs | `mktsb7le6fqr2u1`(FK 列 `nc_jitu___Customers_id` = `coa6ynwcdlune6p`) |
|
||
|
||
> ⚠ ID 是本次重建生成的。若再次删库重建,ID 会变,需同步更新 n8n 节点 + 本文档。
|
||
|
||
## 关系(LinkToAnotherRecord)
|
||
|
||
| 子表 | 字段 | 方向 | 指向 | 说明 |
|
||
|---|---|---|---|---|
|
||
| CustomerProducts | `customer` | bt | Customers | |
|
||
| CustomerProducts | `product` | bt | Products | |
|
||
| Products | `Resources` | **mm** | Resources | M2M link,列 id `c4ko4x8kwi16swg` |
|
||
| Resources | `Products` | mm (reverse) | Products | 列 id `cfxi44n9ixkimqc` |
|
||
| AccessGrants | `customer` | bt | Customers | |
|
||
| AccessGrants | `resource` | bt | Resources | |
|
||
| AccessLogs | `customer` | bt | Customers | |
|
||
| Purchases | `customer` | bt | Customers | |
|
||
| Purchases | `product` | bt | Products | |
|
||
| ReconcileLogs | `customer` | bt | Customers | ⚠ 写入时用 FK 列 `nc_jitu___Customers_id`(v2 忽略 link 列本身) |
|
||
|
||
> M2M 读写方式(已实测):`GET/POST /api/v2/tables/{products}/links/c4ko4x8kwi16swg/records/{productId}`,
|
||
> POST body `{"Id": <resourceId>}`。反向用 Resources 的 `cfxi44n9ixkimqc`。
|
||
|
||
## 表结构
|
||
|
||
### 1. Customers
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| Id | number (PK) | |
|
||
| username | SingleLineText | AList 登录名(唯一) |
|
||
| password | SingleLineText | **plaintext**(规格 §2 已确认,勿 hash) |
|
||
| display_name | SingleLineText | |
|
||
| email | Email | |
|
||
| notes | LongText | |
|
||
| alist_user_id | Number | AList 用户 ID(n8n 回填) |
|
||
| alist_role_id | Number | AList role ID(n8n 回填) |
|
||
| status | SingleSelect | pending/active/suspended/expired/deleted(account-level 整户开关,见下) |
|
||
| sync_status | SingleSelect | pending/syncing/synced/error/unknown |
|
||
| sync_required | Checkbox | 系统维护,管理员勿手改 |
|
||
| last_sync | DateTime | |
|
||
| last_sync_error | LongText | |
|
||
|
||
**Customers.status 语义(account-level 整户开关,不替代 CustomerProduct 到期):**
|
||
|
||
| 值 | 含义 | AList user | scopes |
|
||
|---|---|---|---|
|
||
| `pending` | 已建但未正式开通 | 若无则**不建**;有则 disabled | empty |
|
||
| `active` | 正常使用 | enabled | 按当前有效 CP 重算 |
|
||
| `suspended` | 管理员暂时封禁 | disabled | empty |
|
||
| `expired` | 整户生命周期结束 | disabled | empty |
|
||
| `deleted` | 软删除(保留账号+历史)| disabled | empty |
|
||
|
||
- 任意 `X → active` 恢复时,重算当前有效 grants(忽略已过期产品),不恢复旧 scopes。
|
||
- status 变化**不自动改** CustomerProducts(不批量改 status/expires_at、不删除)。
|
||
|
||
### 2. Products
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| Id | number (PK) | |
|
||
| name | SingleLineText | 如 "Python Basic" |
|
||
| description | LongText | |
|
||
| default_duration_days | Number | 默认有效期天数 |
|
||
| status | SingleSelect | active/inactive |
|
||
| Resources | link (mm) | 该产品包含的 Resources |
|
||
|
||
### 3. Resources
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| Id | number (PK) | |
|
||
| name | SingleLineText | 如 "Python 01" |
|
||
| alist_path | SingleLineText | 真实 AList path,如 "/Class/Python/01" |
|
||
| description | LongText | |
|
||
| status | SingleSelect | active/inactive |
|
||
| Products | link (mm, reverse) | |
|
||
|
||
### 4. CustomerProducts(核心新表)
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| Id | number (PK) | |
|
||
| customer | link bt → Customers | |
|
||
| product | link bt → Products | |
|
||
| start_at | Date | |
|
||
| expires_at | Date | 实际到期(可 override default_duration) |
|
||
| status | SingleSelect | scheduled/active/expired/revoked/suspended |
|
||
| sync_status | SingleSelect | pending/syncing/synced/error/unknown |
|
||
| sync_required | Checkbox | |
|
||
| last_sync | DateTime | |
|
||
| last_sync_error | LongText | |
|
||
|
||
### 5. AccessGrants(最终实际权限,n8n 计算维护)
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| Id | number (PK) | |
|
||
| customer | link bt → Customers | |
|
||
| resource | link bt → Resources | |
|
||
| permission | SingleSelect | read/readwrite |
|
||
| start_at | Date | |
|
||
| expires_at | Date | effective expiry(多来源取 max) |
|
||
| status | SingleSelect | pending/active/revoked/expired |
|
||
| source | SingleSelect | product/manual |
|
||
| sync_status | SingleSelect | pending/syncing/synced/error/unknown |
|
||
| sync_required | Checkbox | |
|
||
| last_sync | DateTime | |
|
||
| last_sync_error | LongText | |
|
||
| revoked_at | DateTime | |
|
||
| revoke_reason | LongText | |
|
||
|
||
### 6. AccessLogs(审计)
|
||
| 字段 | 类型 |
|
||
|---|---|
|
||
| Id | number (PK) |
|
||
| customer | link bt → Customers |
|
||
| username | SingleLineText |
|
||
| resource_path | SingleLineText |
|
||
| file_path | SingleLineText |
|
||
| action | SingleSelect (login/view/download/preview/logout) |
|
||
| ip_address | SingleLineText |
|
||
| user_agent | LongText |
|
||
| result | SingleSelect (success/denied) |
|
||
| bytes | Number |
|
||
| timestamp | DateTime |
|
||
|
||
### 7. ReconcileLogs(W4 对账审计,n8n W4 写入)
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| Id | number (PK) | |
|
||
| run_id | SingleLineText | 本次对账 run(`recon-<ISO>`) |
|
||
| customer | link bt → Customers | FK 列 `nc_jitu___Customers_id`(v2 写链接必须写 FK 列) |
|
||
| drift_type | SingleSelect | user_missing / role_missing / dangling_user_ref / dangling_role_ref / user_disabled_drift / scopes_drift / base_path_drift / permission_drift / role_binding_drift / username_drift / verify_failed / reconcile_error |
|
||
| details | LongText | 前后值 JSON |
|
||
| action | SingleLineText | 做了什么(provisioned / adopted / fixed / rewritten / disabled / log_only / verify …) |
|
||
| result | SingleSelect | repaired / log_only / failed / skipped |
|
||
| timestamp | DateTime | |
|
||
|
||
> 只记 drift / repair / error 行,健康客户不产生行。W4 每日全量或 webhook 触发时写入。
|
||
|
||
### 8. Purchases(纯记录,不参与权限)
|
||
| 字段 | 类型 |
|
||
|---|---|
|
||
| Id | number (PK) |
|
||
| customer | link bt → Customers |
|
||
| product | link bt → Products |
|
||
| purchase_date | Date |
|
||
| amount | Decimal |
|
||
| payment_method | SingleLineText |
|
||
| order_id | SingleLineText |
|
||
| notes | LongText |
|
||
|
||
## 建表 API recipe(实测 2026-08-22,复用见 nocodb-selfhosted skill)
|
||
|
||
- 建 base:`POST /api/v2/meta/bases/ {"title"}`
|
||
- 建表(内联非 select 列):`POST /api/v2/meta/bases/{baseId}/tables {"title","table_name","type":"table","columns":[{"title","uidt"}]}`
|
||
- 可用 uidt:`SingleLineText`,`LongText`,`Number`,`Decimal`,`Date`,`DateTime`,`Email`,`Checkbox`
|
||
- SingleSelect 两段式:
|
||
1. 建空列 `POST /api/v1/db/meta/tables/{tid}/columns {"title","uidt":"SingleSelect"}`
|
||
2. 设选项 **必须同时传 `dtxp`**(逗号枚举串):
|
||
`PATCH /api/v1/db/meta/columns/{colid} {"colOptions":{"options":[{"title","order","color":null}]},"dtxp":"'opt1','opt2'"}`
|
||
⚠ 只传 `colOptions` 不传 `dtxp` 会静默失败(options 空)。
|
||
- Link:`POST /api/v1/db/meta/tables/{childTid}/columns {"title","uidt":"LinkToAnotherRecord","parentId","childId","type":"bt"}`
|
||
- M2M:`type":"mm"`,然后走 `/api/v2/tables/{t}/links/{colId}/records/{rowId}` 读写。
|
||
- 删记录:`DELETE /api/v2/tables/{tid}/records` body `[{"Id":N},...]`(不是 `/records/{id}`)。
|